From db857614a8bb6b8f9858333c1b1de924a3f1f35b Mon Sep 17 00:00:00 2001
From: Florian Reimold <11774314+FlorianReimold@users.noreply.github.com>
Date: Wed, 29 Jan 2025 20:41:45 +0100
Subject: [PATCH] [GH Action] Release page as external sphinx Project (#1965)
- Created Sphinx Project that builds the release page (Release list and individual Download Pages) independent from the eCAL Documentation
- The files are now generated with Jinja2 (not empy anymore)
- The Release Page is not linked anywhere, yet
- The GH Action uploads the release page as .zip file (always) and publishes it to GH Pages (for master builds only)
---
.../workflows/documentation-release-page.yml | 77 ++++
.gitignore | 4 +
doc/_static/css/bignums.css | 188 ++++++++
.../css/sphinx-book-theme-1.1.2-ecaladdon.css | 12 +
doc/_static/css/tabs-3.4.5-ecaladdon.css | 93 ++++
doc/_static/img/ecal-logo.svg | 214 +++++++++
doc/_static/img/favicon.png | Bin 0 -> 1360 bytes
doc/_templates/footer.html | 26 ++
doc/extensions/generate_release_page.py | 431 ++++++++++++++++++
.../resource/release_page.rst.jinja | 206 +++++++++
.../resource/release_page_index.rst.jinja | 44 ++
doc/include.txt | 78 ++++
doc/release_page/conf.py | 126 +++++
.../__init__.py | 431 ++++++++++++++++++
.../resource/release_page.rst.jinja | 206 +++++++++
.../resource/release_page_index.rst.jinja | 49 ++
16 files changed, 2185 insertions(+)
create mode 100644 .github/workflows/documentation-release-page.yml
create mode 100644 doc/_static/css/bignums.css
create mode 100644 doc/_static/css/sphinx-book-theme-1.1.2-ecaladdon.css
create mode 100644 doc/_static/css/tabs-3.4.5-ecaladdon.css
create mode 100644 doc/_static/img/ecal-logo.svg
create mode 100644 doc/_static/img/favicon.png
create mode 100644 doc/_templates/footer.html
create mode 100644 doc/extensions/generate_release_page.py
create mode 100644 doc/extensions/resource/release_page.rst.jinja
create mode 100644 doc/extensions/resource/release_page_index.rst.jinja
create mode 100644 doc/include.txt
create mode 100644 doc/release_page/conf.py
create mode 100644 doc/release_page/generate_release_documentation/__init__.py
create mode 100644 doc/release_page/resource/release_page.rst.jinja
create mode 100644 doc/release_page/resource/release_page_index.rst.jinja
diff --git a/.github/workflows/documentation-release-page.yml b/.github/workflows/documentation-release-page.yml
new file mode 100644
index 0000000000..6472ad3255
--- /dev/null
+++ b/.github/workflows/documentation-release-page.yml
@@ -0,0 +1,77 @@
+name: Documentation Release Page
+
+on:
+ push:
+
+ release:
+ types:
+ - released
+ - unpublished
+ - deleted
+
+jobs:
+ documentation-release-page:
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Get current branch name
+ id: branch
+ run: |
+ # Get the branch name
+ branch=$(echo ${GITHUB_REF#refs/heads/})
+
+ # only publish documentation for master branch
+ if [[ $branch == master ]]; then
+ echo "publish_doc=true" >> $GITHUB_ENV
+ else
+ echo "publish_doc=false" >> $GITHUB_ENV
+ fi
+
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: 'false'
+ fetch-depth: 0
+
+
+ - name: Install Python
+ run: |
+ sudo apt update
+ sudo apt-get -y install python3-dev python3-venv
+
+ - name: Install Python dependencies
+ run: |
+ mkdir ".venv"
+ python3 -m venv ".venv"
+ source ".venv/bin/activate"
+ pip install --upgrade pip
+ pip install -r "$GITHUB_WORKSPACE/doc/requirements.txt"
+
+ - name: Build Release page with Sphinx
+ env:
+ ECAL_GH_API_KEY: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ source ".venv/bin/activate"
+ sphinx-build -b html doc/release_page build/html
+
+ - name: Zip Release Page
+ run: |
+ cd build/html
+ zip -r ../release-page.zip .
+
+ - name: Upload documentation
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-page
+ path: build/release-page.zip
+
+ - name: Deploy Release Page
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ publish_branch: gh-pages
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: build/html
+ destination_dir: releases
+ if: env.publish_doc == 'true'
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 5a4805c230..b1ced1794c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,10 @@ doc/rst/_download_main_page
doc/extensions/_autogen
doc/rst/getting_started/**/*_pb2.py
doc/rst/.venv
+doc/release_page/index.rst
+doc/release_page/ecal_*.rst
+doc/release_page/changelog_*.txt
+doc/release_page/_build
.venv/
*.pyc
diff --git a/doc/_static/css/bignums.css b/doc/_static/css/bignums.css
new file mode 100644
index 0000000000..f4084cbd53
--- /dev/null
+++ b/doc/_static/css/bignums.css
@@ -0,0 +1,188 @@
+.bignums,
+.bignums-hint,
+.bignums-note,
+.bignums-caution,
+.bignums-warning,
+.bignums-attention,
+.bignums-important,
+.bignums-seealso,
+.bignums-tip,
+.bignums-danger,
+.bignums-error,
+.bignums-xxl {
+ padding: 0;
+ counter-reset: li-counter
+}
+
+.bignums>li,
+.bignums-hint>li,
+.bignums-note>li,
+.bignums-caution>li,
+.bignums-warning>li,
+.bignums-attention>li,
+.bignums-important>li,
+.bignums-seealso>li,
+.bignums-tip>li,
+.bignums-danger>li,
+.bignums-error>li,
+.bignums-xxl>li {
+ list-style: none;
+ position: relative;
+ padding: 1rem;
+ padding-left: 3.875rem;
+ padding-top: 1.2875rem;
+ background-color: #e6e6e6;
+ min-height: 4.3rem;
+ border-radius: .25rem
+}
+
+.bignums>li>.first,
+.bignums-hint>li>.first,
+.bignums-note>li>.first,
+.bignums-caution>li>.first,
+.bignums-warning>li>.first,
+.bignums-attention>li>.first,
+.bignums-important>li>.first,
+.bignums-seealso>li>.first,
+.bignums-tip>li>.first,
+.bignums-danger>li>.first,
+.bignums-error>li>.first,
+.bignums-xxl>li>.first {
+ font-weight: 600;
+ font-size: 1.15rem
+}
+
+.bignums>li *:last-child,
+.bignums-hint>li *:last-child,
+.bignums-note>li *:last-child,
+.bignums-caution>li *:last-child,
+.bignums-warning>li *:last-child,
+.bignums-attention>li *:last-child,
+.bignums-important>li *:last-child,
+.bignums-seealso>li *:last-child,
+.bignums-tip>li *:last-child,
+.bignums-danger>li *:last-child,
+.bignums-error>li *:last-child,
+.bignums-xxl>li *:last-child {
+ margin-bottom: 0
+}
+
+.bignums>li:before,
+.bignums-hint>li:before,
+.bignums-note>li:before,
+.bignums-caution>li:before,
+.bignums-warning>li:before,
+.bignums-attention>li:before,
+.bignums-important>li:before,
+.bignums-seealso>li:before,
+.bignums-tip>li:before,
+.bignums-danger>li:before,
+.bignums-error>li:before,
+.bignums-xxl>li:before {
+ font-size: 1.15rem;
+ display: block;
+ position: absolute;
+ top: 1rem;
+ left: 1rem;
+ height: 2em;
+ width: 2em;
+ line-height: 2em;
+ text-align: center;
+ background-color: #313131;
+ color: #fff;
+ border-radius: 50%;
+ content: counter(li-counter, decimal);
+ counter-increment: li-counter;
+ font-weight: 600
+}
+
+.bignums>li+li,
+.bignums-hint>li+li,
+.bignums-note>li+li,
+.bignums-caution>li+li,
+.bignums-warning>li+li,
+.bignums-attention>li+li,
+.bignums-important>li+li,
+.bignums-seealso>li+li,
+.bignums-tip>li+li,
+.bignums-danger>li+li,
+.bignums-error>li+li,
+.bignums-xxl>li+li {
+ margin-top: 1rem
+}
+
+.bignums-hint>li,
+.bignums-note>li {
+ background-color: #ebf7fb
+}
+
+.bignums-hint>li:before,
+.bignums-note>li:before {
+ background-color: #5bc0de;
+ color: #212121
+}
+
+.bignums-caution>li,
+.bignums-warning>li,
+.bignums-attention>li {
+ background-color: #fdf5ea
+}
+
+.bignums-caution>li:before,
+.bignums-warning>li:before,
+.bignums-attention>li:before {
+ background-color: #f0ad4e;
+ color: #212121
+}
+
+.bignums-important>li,
+.bignums-seealso>li,
+.bignums-tip>li {
+ background-color: #ebf6eb
+}
+
+.bignums-important>li:before,
+.bignums-seealso>li:before,
+.bignums-tip>li:before {
+ background-color: #5cb85c;
+ color: #fff
+}
+
+.bignums-danger>li,
+.bignums-error>li {
+ background-color: #faeaea
+}
+
+.bignums-danger>li:before,
+.bignums-error>li:before {
+ background-color: #d9534f;
+ color: #fff
+}
+
+.bignums-xxl>li {
+ padding: 0;
+ padding-left: 3.75rem;
+ padding-top: .375rem;
+ background-color: transparent;
+ min-height: 3rem
+}
+
+.bignums-xxl>li>.first {
+ font-size: 1.5rem
+}
+
+.bignums-xxl>li:before {
+ font-size: 1.5rem;
+ top: 0;
+ left: 0
+}
+
+.bignums-xxl>li+li {
+ border-top: 1px solid rgba(0, 0, 0, 0.15);
+ margin-top: 1.375rem;
+ padding-top: 1.375rem
+}
+
+.bignums-xxl>li+li:before {
+ top: 1rem
+}
\ No newline at end of file
diff --git a/doc/_static/css/sphinx-book-theme-1.1.2-ecaladdon.css b/doc/_static/css/sphinx-book-theme-1.1.2-ecaladdon.css
new file mode 100644
index 0000000000..0ff1c1d2c1
--- /dev/null
+++ b/doc/_static/css/sphinx-book-theme-1.1.2-ecaladdon.css
@@ -0,0 +1,12 @@
+html[data-theme=dark],
+html[data-theme=light] {
+ --pst-color-primary: #ffa500;
+}
+
+html[data-theme=light] {
+ --pst-color-secondary: #CC8400;
+}
+
+html[data-theme=dark] {
+ --pst-color-secondary: #FFC04D;
+}
\ No newline at end of file
diff --git a/doc/_static/css/tabs-3.4.5-ecaladdon.css b/doc/_static/css/tabs-3.4.5-ecaladdon.css
new file mode 100644
index 0000000000..07065503ea
--- /dev/null
+++ b/doc/_static/css/tabs-3.4.5-ecaladdon.css
@@ -0,0 +1,93 @@
+.sphinx-tabs {
+ margin-bottom: 1rem;
+}
+
+[role="tablist"] {
+ border-bottom: 1px solid #a0a0a0;
+}
+
+.sphinx-tabs-tab {
+ position: relative;
+ font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
+ color: var(--pst-color-muted);
+ line-height: 24px;
+ margin: 0;
+ font-size: 16px;
+ font-weight: 400;
+ background-color: var(--pst-color-background);
+ border-radius: 5px 5px 0 0;
+ border: 0;
+ padding: 1rem 1.5rem;
+ margin-bottom: 0;
+}
+
+.sphinx-tabs-tab[aria-selected="true"] {
+ font-weight: 700;
+ border: 1px solid #a0a0a0;
+ border-bottom: 1px solid var(--pst-color-background);
+ margin: -1px;
+ background-color: var(--pst-color-background);
+
+ box-shadow: 0 9px 0px -1px var(--pst-color-background), 0 3px 3px 0 var(--pst-color-shadow);
+}
+
+.sphinx-tabs-tab:focus {
+ z-index: 1;
+ outline-offset: 1px;
+}
+
+.sphinx-tabs-panel {
+ position: relative;
+ padding: 1rem;
+ border: 1px solid #a0a0a0;
+ margin: 0px -1px -1px -1px;
+ border-radius: 0 0 5px 5px;
+ border-top: 0;
+ background: var(--pst-color-background);
+
+ box-shadow: 0 3px 3px 0px var(--pst-color-shadow);
+}
+
+.sphinx-tabs-panel.code-tab {
+ padding: 0.4rem;
+}
+
+.sphinx-tab img {
+ margin-bottom: 24 px;
+}
+
+/* Dark theme preference styling */
+
+@media (prefers-color-scheme: dark) {
+ body[data-theme="auto"] .sphinx-tabs-panel {
+ color: white;
+ background-color: rgb(50, 50, 50);
+ }
+
+ body[data-theme="auto"] .sphinx-tabs-tab {
+ color: white;
+ background-color: rgba(255, 255, 255, 0.05);
+ }
+
+ body[data-theme="auto"] .sphinx-tabs-tab[aria-selected="true"] {
+ border-bottom: 1px solid rgb(50, 50, 50);
+ background-color: rgb(50, 50, 50);
+ }
+}
+
+/* Explicit dark theme styling */
+
+body[data-theme="dark"] .sphinx-tabs-panel {
+ color: white;
+ background-color: rgb(50, 50, 50);
+}
+
+body[data-theme="dark"] .sphinx-tabs-tab {
+ color: white;
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+body[data-theme="dark"] .sphinx-tabs-tab[aria-selected="true"] {
+ border-bottom: 2px solid rgb(50, 50, 50);
+ background-color: rgb(50, 50, 50);
+}
diff --git a/doc/_static/img/ecal-logo.svg b/doc/_static/img/ecal-logo.svg
new file mode 100644
index 0000000000..3dd4b18c12
--- /dev/null
+++ b/doc/_static/img/ecal-logo.svg
@@ -0,0 +1,214 @@
+
+
diff --git a/doc/_static/img/favicon.png b/doc/_static/img/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe298c679a304a19215b7dd60aa3ba03fac51c7a
GIT binary patch
literal 1360
zcmV-W1+V&vP)tQ
zK~z|UwU=vbR7DiWe{*-&ZGm=$VpouWm|`#jNonndr~#_Nhe8l67{dpm0etX`NW()&
zLkegk1X`m32?mYv0R$=_BB2cmK`3nyP%BhW5ok+V(iSM)TkiG4*~{K#?{>R@|Kw!m
z&YhWaX3m-anNiqKwSp{DV>TwaDEa^%?hka}5>SDnG7NS+TScRg%YWp8y
z0L57yELDXVZh&extAPTgz@G|fDd6=)S`yTucn@qpD&P45C>RGGcYKW|(5^#cE7Uim
znXstJg$XQCp>3~J3YFo9)MV#Ligv=s`y<?5=$&$V
zuF>jv5kbH+e4umm-PB?ER`}v@^gk6cMk#o9FuXPro*M#MoG>hl2TZbC=RX-sWB
zyuJx)EVm>huo9QgbYe02nV{9R-~ZD!3I9n%eUN?*%4%0_NS#|X3Ie8&ujAg*yU!tR20Wami
z>Q|uK1J26rFibdb8a5wBbu7&}brn$>7oXouGW{nFsO6Hs*?nyy9n
z3F>l0GZ#bF5;*oJeD(%ZybBYu!R_GNZ8lw<(#TC6*6odKOHGEy(&5pEL{46S^3&n}
zAMJFl`j!1s+CBwH1A;S-P2Nds)oOeN<
zLl9;SjK5uWIP!xDC>Svaw$1N+e3kG(I0zF#Z9U`_i*m0UMm_<4AN1-WQGc`+%75+J
zTyb;Romp^be>idmZUkkEH%y1YeZ&1rzK8dB!CRYQ*FqQ+gF7t5-GSmg!Ve8CA_q>3
z^86gwyC_mQz?`u#K1;}H`bH`5$p9yQ@O6Xr)11jkDOfWV2KNzH&D#=9L7NEA(%F2>F&~zOR{swuO(9Ij$h=J*F=nwd}NsjIHcBniDx*-7!cfkXVtuUoT
z(sG6$w$2yEi-pX^(YxjM!6!4Mo^TALrNB3HCHiaYVe&d>cNiw@u7<1EJD;x$cUos7
zzMcy&ub1oC^DD*IO2MQYxi^&lAXM!HdoGB3CF=~0tuU&{IZcDaz??B~rVe&gN}H7n
zPDw7%-N=h&Kzk69x^U^RBP|77A*=1#(Bpy+rozOhz!h@xvWekwZNb~siyBY?&=_MC
z@t{Hm*t21;9&&tLF(S;;rJ!6xQ3@FK&64XAg~byuu7OEwr9Q=hJ1n%cK|?bDhB7E|
zZ7k>V%v_RzXRcm@TP8f<2A8XAF=6M49!c=(NV!MYS8qMd>YCv~V<>Bztjb!>m*K~$
zo0UKTcS+9)z%b#x><*TxLX3z-R<0mZ)0k~SF7OEV_j!I1IAAC~%vs6_>&Jf~n71f7
SaNHIE0000
+ .links-container ul {
+ list-style-type: none;
+ padding: 0;
+ }
+
+ .links-container ul li {
+ display: inline;
+ margin-right: 10px;
+ }
+
+ .links-container ul li:last-child {
+ margin-right: 0;
+ }
+
+
diff --git a/doc/extensions/generate_release_page.py b/doc/extensions/generate_release_page.py
new file mode 100644
index 0000000000..48ca6a4a23
--- /dev/null
+++ b/doc/extensions/generate_release_page.py
@@ -0,0 +1,431 @@
+import os
+
+import sys
+import re
+import github
+import jinja2
+import semantic_version
+from collections import OrderedDict
+
+ubuntu_default_python_version_dict = \
+{
+ semantic_version.Version("18.4.0"): semantic_version.Version("3.6.0"),
+ semantic_version.Version("20.4.0"): semantic_version.Version("3.8.0"),
+ semantic_version.Version("22.4.0"): semantic_version.Version("3.10.0"),
+ semantic_version.Version("24.4.0"): semantic_version.Version("3.12.0"),
+}
+
+ubuntu_codename_dict = \
+{
+ "noble": semantic_version.Version("24.4.0"),
+ "jammy": semantic_version.Version("22.4.0"),
+ "focal": semantic_version.Version("20.4.0"),
+ "bionic": semantic_version.Version("18.4.0"),
+ "xenial": semantic_version.Version("16.4.0"),
+ "trusty": semantic_version.Version("14.4.0"),
+}
+
+"""
+Retrieves a dictionary of release branches and their corresponding releases from the GitHub repository.
+Args:
+ gh_repo (github.Github): An authenticated GitHub instance.
+Returns:
+ dict: A dictionary where the keys are minor relase numbers (-> patch level
+ set to 0) and Values are Dictionaries of
+ {specific_release : gh_release_object}
+Example:
+ {
+ Version('5.8.0'): {
+ Version('5.8.0'): ,
+ Version('5.8.1'):
+ },
+ Version('5.9.0'): {
+ Version('5.9.0'):
+ }
+ }
+"""
+def get_releases_dict(gh_repo):
+ gh_releases = gh_repo.get_releases()
+
+ gh_release_branches_dict = OrderedDict()
+
+ for gh_release in gh_releases:
+ if gh_release.prerelease or gh_release.draft:
+ continue
+
+ version_string = gh_release.tag_name
+
+ if version_string.startswith("v") or version_string.startswith("V"):
+ version_string = version_string[1:]
+ if version_string.startswith("."):
+ version_string = version_string[1:]
+
+ # Fix format, so it can be parsed by semantic_version:
+ dot_components = version_string.split(".")
+ if len(dot_components) == 4:
+ version_string = '.'.join(dot_components[:-1]) + "+" + dot_components[3]
+ elif len(dot_components) == 5:
+ version_string = '.'.join(dot_components[:-2]) + "-" + dot_components[3] + "+" + dot_components[4]
+
+ try:
+ version = semantic_version.Version(version_string)
+ except:
+ sys.stderr.write("Warning: eCAL Release \"" + gh_release.tag_name + "\" is not parsable to a proper version.\n")
+ continue
+
+ version = semantic_version.Version(version_string)
+ release_branch = semantic_version.Version(major = version.major, minor = version.minor, patch = 0)
+
+ if not release_branch in gh_release_branches_dict:
+ # Initialize dicitonary for this branch
+ gh_release_branches_dict[release_branch] = OrderedDict()
+
+ gh_release_branches_dict[release_branch][version] = gh_release
+
+ # Sort the minor eCAL Release branches by version
+ gh_release_branches_dict = OrderedDict(sorted(gh_release_branches_dict.items(), key = lambda x: x[0], reverse = True))
+
+ # Sort the specific releases by version
+ for release_branch in gh_release_branches_dict:
+ gh_release_branches_dict[release_branch] = OrderedDict(sorted(gh_release_branches_dict[release_branch].items(), key = lambda x: x[0], reverse = True))
+
+ return gh_release_branches_dict
+
+"""
+Retrieves the properties of a given GitHub asset.
+
+Args:
+ ecal_version (semantic_version.Version): The version of eCAL.
+ gh_asset (github.Asset): The GitHub asset object.
+
+Returns:
+ dict: A dictionary containing the properties of the asset, including:
+
+ {
+ 'filename' : 'actual filename',
+ 'download_link': 'browser download link',
+ 'type' : 'source / ecal_installer / python_binding',
+ 'properties' : SEE BELOW,
+ }
+
+ Properties for "source":
+ {}
+
+ Properties for "ecal_installer":
+ {
+ 'os': 'windows / macos / ubuntu',
+ 'os_version': Semver('0.0.0') (Only for Ubuntu specific installers),
+ 'cpu': 'amd64 / arm64'
+ }
+
+ Properties for "python_binding":
+ {
+ 'os': 'manylinux / macos / windows / ubuntu',
+ 'os_version': Semver('0.0.0') (Only for Ubuntu specific bindings),
+ 'cpu': 'amd64 / arm64',
+ 'python_version': Semver('3.6.0') (example)
+ 'python_implementation': 'cp / pp etc.' (i.e. CPython or PyPy)
+ }
+"""
+def get_asset_properties(ecal_version, gh_asset):
+ asset_properties = {
+ 'filename' : '',
+ 'download_link': '',
+ 'type' : '',
+ 'properties' : {},
+ }
+
+ asset_properties['filename'] = gh_asset.name
+ asset_properties['download_link'] = gh_asset.browser_download_url
+
+ # Source
+ if asset_properties['filename'].endswith('tar.gz'):
+ asset_properties['type'] = 'source'
+
+ # eCAL Installer for Windows
+ elif asset_properties['filename'].endswith('.msi') or asset_properties['filename'].endswith('.exe'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'windows'
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # eCAL installer for macOS
+ elif asset_properties['filename'].endswith('.dmg'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'macos'
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # eCAL Installer for Linux
+ elif asset_properties['filename'].endswith('.deb'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'ubuntu'
+ asset_properties['properties']['os_version'] = semantic_version.Version('0.0.0')
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+ else:
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+
+ # Python binding (whl)
+ elif asset_properties['filename'].endswith('.whl'):
+ asset_properties['type'] = 'python_binding'
+
+ # Get Operating system
+ if 'manylinux' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'manylinux'
+ elif 'darwin' in asset_properties['filename'] or 'macos' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'macos'
+ elif "win64" in asset_properties['filename'] or "win_amd64" in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'windows'
+ elif 'linux' in asset_properties['filename']:
+ # Old eCAL 5.x wheels were Ubuntu specific
+ asset_properties['properties']['os'] = 'ubuntu'
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+ else:
+ sys.stderr.write("Warning: Unable to determine OS of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get Python CPU Architecture
+ filename_without_extension = os.path.splitext(asset_properties['filename'])[0]
+ if filename_without_extension.endswith('amd64') or filename_without_extension.endswith('x86_64') or filename_without_extension.endswith('win64'):
+ asset_properties['properties']['cpu'] = 'amd64'
+ elif filename_without_extension.endswith('arm64') or filename_without_extension.endswith('aarch64'):
+ asset_properties['properties']['cpu'] = 'arm64'
+ else:
+ sys.stderr.write("Warning: Unable to determine CPU Architecture of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get Python version
+ python_version = semantic_version.Version("0.0.0")
+ python_implementation = ''
+ components = asset_properties['filename'][:-4].split('-')
+ # The python version is either index 2 or 3, depending on whether the optional build tag is used.
+ for index in range(2,4):
+ if re.match(r"[a-z]{2}[0-9]+", components[index]):
+ python_implementation = components[index][:2]
+ python_version_major_string = components[index][2]
+ python_verstion_minor_string = "0"
+ if len(components[index]) > 3:
+ python_verstion_minor_string = components[index][3:]
+ python_version = semantic_version.Version(python_version_major_string + "." + python_verstion_minor_string + ".0")
+ break
+
+ if python_version == semantic_version.Version("0.0.0"):
+ sys.stderr.write("Warning: Unable to determine python version: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+ if not python_implementation:
+ sys.stderr.write("Warning: Unable to determine python implementation: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ asset_properties['properties']['python_version'] = python_version
+ asset_properties['properties']['python_implementation'] = python_implementation
+
+ # Python binding (.egg)
+ elif asset_properties['filename'].endswith('.egg'):
+ asset_properties['type'] = 'python_binding'
+
+ # Get Operating system
+ if 'darwin' in asset_properties['filename'] or 'macos' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'macos'
+ elif "win64" in asset_properties['filename'] or "win_amd64" in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'windows'
+ elif 'linux' in asset_properties['filename'] or 'bionic' in asset_properties['filename'] or 'focal' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'ubuntu'
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+ else:
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+
+ if not asset_properties['properties'].get('os'):
+ sys.stderr.write("Warning: Unable to determine OS of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get python version
+ python_version = semantic_version.Version("0.0.0")
+ python_match_result = re.findall(r"py[0-9]+\.[0-9]+", asset_properties['filename'])
+ if len(python_match_result) > 0:
+ python_version = semantic_version.Version(python_match_result[0][2:] + ".0")
+
+ if python_version == semantic_version.Version("0.0.0"):
+ sys.stderr.write("Warning: Unable to determine python version: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ asset_properties['properties']['python_version'] = python_version
+ asset_properties['properties']['python_implementation'] = 'cp' # The eggs were all CPython
+
+ # CPU Architecture was always amd64 back then
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # Warning, as we have no idea what this file is for
+ else:
+ sys.stderr.write("Warning: Unknown asset type: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ return asset_properties
+
+def generate_release_index_page(releases_dict, list_of_supported_minor_versions, output_filename):
+ # Create the output directory if it does not exist
+ output_dir = os.path.dirname(output_filename)
+ if output_dir and not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Load the Jinja2 template
+ template_loader = jinja2.FileSystemLoader(searchpath="resource/")
+ template_env = jinja2.Environment(loader=template_loader)
+ template_file = "release_page_index.rst.jinja"
+ template = template_env.get_template(template_file)
+
+ # Render the template with the context
+ context = {
+ 'releases_dict': releases_dict,
+ 'list_of_supported_minor_versions': list_of_supported_minor_versions,
+ 'group_asset_list_by_os_and_arch': group_asset_list_by_os_and_arch,
+ 'get_rst_release_page_label': get_rst_release_page_label,
+ }
+ output = template.render(context)
+
+ # Save the rendered template to a file
+ with open(output_filename, "w") as f:
+ f.write(output)
+
+def generate_release_page(gh_release,
+ ecal_version,
+ asset_list,
+ minor_is_supported,
+ latest_version_of_minor,
+ output_dir):
+ ecal_version_string = str(ecal_version).replace('.', '_').replace('-', '_').replace('+', '_')
+ output_filename = "ecal_" + ecal_version_string + ".rst"
+ changelog_file = "changelog_ecal_" + ecal_version_string + ".txt"
+
+ # Create the output directory if it does not exist
+ if output_dir and not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Save the changelog to a file
+ changelog = gh_release.body
+ changelog = changelog.replace('\r\n', '\n')
+ with open(os.path.join(output_dir, changelog_file), "w") as f:
+ f.write(changelog)
+
+ # Load the Jinja2 template
+ template_loader = jinja2.FileSystemLoader(searchpath="resource/")
+ template_env = jinja2.Environment(loader=template_loader)
+ template_file = "release_page.rst.jinja"
+ template = template_env.get_template(template_file)
+
+ # Render the template with the context
+ context = {
+ 'gh_release': gh_release,
+ 'ecal_version': ecal_version,
+ 'asset_list': asset_list,
+ 'changelog_file_path': changelog_file,
+ 'minor_is_supported': minor_is_supported,
+ 'latest_version_of_minor': latest_version_of_minor,
+ 'ubuntu_default_python_version_dict': ubuntu_default_python_version_dict,
+ 'group_asset_list_by_os_and_arch': group_asset_list_by_os_and_arch,
+ 'get_rst_release_page_label': get_rst_release_page_label,
+ }
+ output = template.render(context)
+
+ # Save the rendered template to a file
+ path = os.path.join(output_dir, output_filename)
+ with open(path, "w") as f:
+ f.write(output)
+
+"""
+Groups a list of assets by their OS, OS version, and CPU architecture.
+Args:
+ asset_list (list): A list of asset properties dictionaries.
+Returns:
+ dict: A dictionary where the keys are tuples of (os, os_version, cpu) and
+ the values are lists of asset properties dictionaries.
+"""
+def group_asset_list_by_os_and_arch(asset_list):
+ os_arch_dict = OrderedDict()
+
+ for asset_properties in asset_list:
+ os = asset_properties['properties'].get('os', 'unknown')
+ os_version = asset_properties['properties'].get('os_version', semantic_version.Version('0.0.0'))
+ cpu = asset_properties['properties'].get('cpu', 'unknown')
+
+ key = (os, os_version, cpu)
+ if key not in os_arch_dict:
+ os_arch_dict[key] = []
+ os_arch_dict[key].append(asset_properties)
+
+ return os_arch_dict
+
+def get_rst_release_page_label(ecal_version):
+ return "ecal_release_page_" + str(ecal_version).replace('.', '_').replace('-', '_').replace('+', '_')
+
+def generate_release_documentation(gh_api_key, index_filepath, release_dir_path):
+ gh = github.Github(gh_api_key)
+ gh_repo = gh.get_repo("eclipse-ecal/ecal")
+ releases_dict = get_releases_dict(gh_repo)
+
+ list_of_supported_minor_versions = list(releases_dict.keys())[:2]
+
+ generate_release_index_page(releases_dict,
+ list_of_supported_minor_versions,
+ index_filepath)
+
+ for minor_version in releases_dict:
+
+ # Get the latest eCAL Version of this release branch
+ latest_release_for_this_minor = list(releases_dict[minor_version].keys())[0]
+ this_minor_is_supported = minor_version in list_of_supported_minor_versions
+
+ for ecal_version in releases_dict[minor_version]:
+
+ # Check the support status of this release
+
+ gh_release = releases_dict[minor_version][ecal_version]
+ gh_asset_list = gh_release.get_assets()
+
+ asset_list = []
+
+ for gh_asset in gh_asset_list:
+ asset_properties = get_asset_properties(ecal_version, gh_asset)
+ asset_list.append(asset_properties)
+
+ # Sort the asset list by:
+ # 1. OS (windows > manylinux > ubuntu > macos)
+ # 2. OS version (descending)
+ # 3. CPU (amd64 > arm64)
+ # 4. Python version (descending)
+
+ asset_list.sort(key = lambda x: x['properties'].get('python_version', semantic_version.Version("0.0.0")), reverse = True)
+ asset_list.sort(key = lambda x: x['properties'].get('os_version', semantic_version.Version("0.0.0")), reverse = True)
+ asset_list.sort(key = lambda x: x['properties'].get('cpu', 'unknown'))
+ asset_list.sort(key = lambda x: ['windows', 'manylinux', 'ubuntu', 'macos'].index(x['properties'].get('os', 'unknown')) if x['properties'].get('os', 'unknown') in ['windows', 'manylinux', 'ubuntu', 'macos'] else float('inf'))
+
+ generate_release_page(gh_release,
+ ecal_version,
+ asset_list,
+ this_minor_is_supported,
+ latest_release_for_this_minor,
+ release_dir_path)
+
+if __name__=="__main__":
+ gh_api_key = os.getenv("ECAL_GH_API_KEY")
+ if gh_api_key:
+ generate_release_documentation(gh_api_key, "index.rst", "release")
+ else:
+ sys.stderr.write("ERROR: Environment variable ECAL_GH_API_KEY not set. Without an API key, GitHub will not provide enough API calls to generate the download tables.\n")
+ exit(1)
+
+
+
+
+
+
+
diff --git a/doc/extensions/resource/release_page.rst.jinja b/doc/extensions/resource/release_page.rst.jinja
new file mode 100644
index 0000000000..4411a39b01
--- /dev/null
+++ b/doc/extensions/resource/release_page.rst.jinja
@@ -0,0 +1,206 @@
+{% set ecal_version_string = ecal_version | string -%}
+
+{# Makro for properly naming the OS, possibly with icon. e.g. "|fa-windows| Windows" (-> capitalized) or |fa-ubuntu| Ubuntu 24.04 (-> with Version number) -#}
+{% macro get_os_string(asset, with_icon) -%}
+ {% if asset['properties']['os'] == 'windows' -%}
+ {% set os_string = (with_icon and '|fa-windows| ' or '') ~ 'Windows' -%}
+ {% elif asset['properties']['os'] == 'macos' -%}
+ {% set os_string = (with_icon and '|fa-apple| ' or '') ~ 'macOS' -%}
+ {% elif asset['properties']['os'] == 'ubuntu' -%}
+ {% set os_version_padded = asset['properties']['os_version'].major ~ '.' ~ '%02d' | format(asset['properties']['os_version'].minor) -%}
+ {% set os_string = (with_icon and '|fa-Ubuntu| ' or '') ~ 'Ubuntu ' ~ os_version_padded -%}
+ {% elif asset['properties']['os'] == 'manylinux' -%}
+ {% set os_string = (with_icon and '|fa-linux| ' or '') ~ 'Linux (All)' -%}
+ {% elif asset['properties']['os'] == '' -%}
+ {% set os_string = 'Unknown' -%}
+ {% else -%}
+ {% set os_string = asset['properties']['os'].capitalize() -%}
+ {% endif -%}
+ {{ os_string -}}
+{% endmacro -%}
+
+{# Makro for naming the CPU Architecture. Mainly to make amd64 to x64, so the user can more easily distinguish it from arm64 #}
+{% macro get_cpu_string(asset) -%}
+ {% if asset['properties']['cpu'] == 'amd64' -%}
+ {% set cpu_string = 'x64' -%}
+ {% elif asset['properties']['cpu'] == 'arm64' -%}
+ {% set cpu_string = 'ARM64' -%}
+ {% else -%}
+ {% set cpu_string = asset['properties']['cpu'] -%}
+ {% endif -%}
+ {{ cpu_string -}}
+{% endmacro -%}
+:orphan:
+
+.. include:: /../include.txt
+
+.. _{{ get_rst_release_page_label(ecal_version) }}:
+
+===============
+eCAL {{ ecal_version_string }}
+===============
+
+{% if not minor_is_supported %}
+.. warning::
+
+ eCAL {{ ecal_version.major }}.{{ ecal_version.minor }} is not supported anymore. Please consider upgrading to a newer version.
+{% endif %}
+
+- Release Date: {{ gh_release.published_at.strftime('%Y-%m-%d') }}
+- GitHub Release Page: {{ gh_release.html_url }}
+
+Changelog
+=========
+
+.. literalinclude:: {{ changelog_file_path }}
+ :language: text
+
+Downloads
+=========
+
+{% if ecal_version < latest_version_of_minor %}
+.. note::
+
+ A more recent version of eCAL {{ ecal_version.major }}.{{ ecal_version.minor }} is available: :ref:`eCAL {{ latest_version_of_minor }} <{{ get_rst_release_page_label(latest_version_of_minor) }}>`.
+{% endif %}
+
+{# Get the ecal_installers from the list and remove them from the main list. -#}
+{% set ecal_installer_list = asset_list | selectattr('type', 'equalto', 'ecal_installer') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'ecal_installer') | list -%}
+
+{% if ecal_installer_list %}
+eCAL Installer
+--------------
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% for asset in ecal_installer_list -%}
+
+ * - {{ get_os_string(asset, true) }}
+
+ - {{ get_cpu_string(asset) }}
+
+ - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the python bindings from the list -#}
+{% set python_binding_list = asset_list | selectattr('type', 'equalto', 'python_binding') | selectattr('properties.python_implementation', 'equalto', 'cp') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'python_binding') | list -%}
+
+{% if python_binding_list %}
+|fa-python| Python Binding
+--------------------------
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% set python_binding_grouped_dict = group_asset_list_by_os_and_arch(python_binding_list) -%}
+
+ {% for key, value in python_binding_grouped_dict.items() -%}
+ {% if value[0]['properties']['os'] == 'ubuntu' -%}
+ {% set default_python_version = ubuntu_default_python_version_dict[value[0]['properties']['os_version']] -%}
+ {% endif %}
+ * - {{ get_os_string(value[0], true) }}
+
+ - {{ get_cpu_string(value[0]) }}
+
+ - {% for asset in value -%}
+ `Python {{ asset['properties']['python_version'].major }}.{{ asset['properties']['python_version'].minor }} (.{{ asset['filename'].split('.')[-1] }}) <{{ asset['download_link'] }}>`__ {% if asset['properties']['python_version'] == default_python_version %} (Default){% endif %}
+
+ {% endfor -%}
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the PyPy bindings from the list -#}
+{% set pypy_binding_list = asset_list | selectattr('type', 'equalto', 'python_binding') | selectattr('properties.python_implementation', 'equalto', 'pp') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'python_binding') | list -%}
+
+{% if pypy_binding_list %}
+|fa-python| PyPy Binding
+------------------------
+
+*PyPy bindings are not compatibly with the regular Python implementation.*
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% set pypy_binding_grouped_dict = group_asset_list_by_os_and_arch(pypy_binding_list) -%}
+
+ {% for key, value in pypy_binding_grouped_dict.items() %}
+ * - {{ get_os_string(value[0], true) }}
+
+ - {{ get_cpu_string(value[0]) }}
+
+ - {% for asset in value -%}
+ `PyPy {{ asset['properties']['python_version'].major }}.{{ asset['properties']['python_version'].minor }} (.{{ asset['filename'].split('.')[-1] }}) <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the source files from the list -#}
+{% set source_file_list = asset_list | selectattr('type', 'equalto', 'source') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'source') | list -%}
+
+{% if source_file_list %}
+|fa-code| Source
+----------------
+
+.. list-table::
+ :widths: 1
+
+ {% for asset in source_file_list -%}
+
+ * - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif %}
+
+{# Get the other files from the list -#}
+{% if asset_list %}
+Other Downloads
+---------------
+
+.. list-table::
+ :widths: 1
+ :header-rows: 1
+
+ * - Files
+
+ {% for asset in asset_list -%}
+
+ * - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif -%}
\ No newline at end of file
diff --git a/doc/extensions/resource/release_page_index.rst.jinja b/doc/extensions/resource/release_page_index.rst.jinja
new file mode 100644
index 0000000000..4b3deac6b5
--- /dev/null
+++ b/doc/extensions/resource/release_page_index.rst.jinja
@@ -0,0 +1,44 @@
+.. include:: /../include.txt
+
+.. _all_releases:
+
+============
+All releases
+============
+
+Here you can find a list of all eCAL Versions ever released on GitHub.
+
+{% for minor_version, releases_dict in releases_dict.items() %}
+eCAL {{ minor_version.major }}.{{ minor_version.minor }}
+===========
+
+{% if minor_version not in list_of_supported_minor_versions -%}
+
+*eCAL {{ minor_version.major }}.{{ minor_version.minor }} has reached its end of life.*
+
+{% endif -%}
+
+.. list-table::
+ :widths: 3 3 4
+ :header-rows: 1
+
+ * - Release
+
+ - Release Date
+
+ - Support Status
+
+{% for ecal_version, gh_release in releases_dict.items() -%}
+{% set is_latest_release_in_this_minor = (ecal_version == releases_dict.keys()|list|first) %}
+ * - :ref:`eCAL {{ gh_release.tag_name }} <{{ get_rst_release_page_label(ecal_version) }}>`
+
+ - {{ gh_release.published_at.strftime("%Y-%m-%d") }}
+
+ - {% if (minor_version in list_of_supported_minor_versions) and is_latest_release_in_this_minor -%}
+ Supported
+ {% else -%}
+ EOL
+ {% endif -%}
+{% endfor -%}
+
+{% endfor -%}
\ No newline at end of file
diff --git a/doc/include.txt b/doc/include.txt
new file mode 100644
index 0000000000..0907e80e11
--- /dev/null
+++ b/doc/include.txt
@@ -0,0 +1,78 @@
+
+.. |fa-windows| raw:: html
+
+
+
+.. |fa-linux| raw:: html
+
+
+
+.. |fa-ubuntu| raw:: html
+
+
+
+.. |fa-apple| raw:: html
+
+
+
+.. |fa-folder-open| raw:: html
+
+
+
+.. |fa-folder| raw:: html
+
+
+
+.. |fa-file| raw:: html
+
+
+
+.. |fa-file-alt| raw:: html
+
+
+
+.. |fa-download| raw:: html
+
+
+
+.. |fa-github| raw:: html
+
+
+
+.. |fa-book| raw:: html
+
+
+
+.. |fa-copy| raw:: html
+
+
+
+.. |fa-python| raw:: html
+
+
+
+.. |fa-pencil-alt| raw:: html
+
+
+
+.. |fa-code| raw:: html
+
+
+
+
+.. |ecalini-path-windows| replace:: :file:`C:\\ProgramData\\eCAL\\ecal.yaml`
+.. |ecalini-path-ubuntu| replace:: :file:`/etc/ecal/ecal.yaml`
+
+.. |ecalini-path-windows-old| replace:: :file:`C:\\eCAL\\cfg\\ecal.yaml`
+.. |ecalini-path-ubuntu-old| replace:: :file:`/usr/etc/ecal/ecal.yaml`
+
+
+.. |person_snd-path-windows| replace:: :file:`C:\\eCAL\\samples\\bin\\ecal_sample_person_snd.exe`
+.. |person_rec-path-windows| replace:: :file:`C:\\eCAL\\samples\\bin\\ecal_sample_person_rec.exe`
+.. |service_server_c-path-windows| replace:: :file:`C:\\eCAL\\samples\\bin\\ecal_sample_minimal_server_c.exe`
+.. |service_client_c-pathwindows| replace:: :file:`C:\\eCAL\\samples\\bin\\ecal_sample_minimal_client_c.exe`
+
+.. |ecal_mon-start-menu-path-windows| replace:: :file:`Start / eCAL / eCAL Monitor`
+.. |ecal_rec-start-menu-path-windows| replace:: :file:`Start / eCAL / eCAL Recorder`
+.. |ecal_play-start-menu-path-windows| replace:: :file:`Start / eCAL / eCAL Player`
+.. |ecal_sys-start-menu-path-windows| replace:: :file:`Start / eCAL / eCAL Sys`
diff --git a/doc/release_page/conf.py b/doc/release_page/conf.py
new file mode 100644
index 0000000000..4c6132a61b
--- /dev/null
+++ b/doc/release_page/conf.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import os
+import sys
+
+# -- Generate download archive and tables for the homepage --------------------
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import generate_release_documentation
+
+gh_api_key = os.getenv("ECAL_GH_API_KEY")
+if gh_api_key:
+ release_page_dir = "."
+ index_file_path = "./index.rst"
+ generate_release_documentation.generate_release_documentation(gh_api_key, index_file_path, release_page_dir)
+else:
+ print("ERROR: Environment variable ECAL_GH_API_KEY not set. Skipping generating download tables.")
+ exit(1)
+
+# -- Project information -----------------------------------------------------
+
+project = u'Eclipse eCALâ„¢'
+copyright = u'2023, Continental'
+#author = u'Continental'
+
+# The short X.Y version
+# version = u''
+# The full version, including alpha/beta/rc tags
+# release = u''
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx_book_theme',
+ 'sphinx_tabs.tabs',
+ 'sphinx.ext.githubpages',
+]
+
+# Tell sphinx what the primary language being documented is.
+#primary_domain = 'cpp'
+
+# Tell sphinx what the pygments highlight language should be.
+#highlight_language = 'cpp'
+
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['../_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+# pygments_style = 'sphinx'
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'sphinx_book_theme'
+
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['../_static']
+
+html_css_files = [
+ 'css/bignums.css', # Enable the bignum feature from the sphinx-typo3-theme
+ 'css/sphinx-book-theme-1.1.2-ecaladdon.css', # Change colors of the sphinx-book-theme
+ 'css/tabs-3.4.5-ecaladdon.css', # Change colors of the sphinx-tabs
+]
+
+html_title = "Eclipse eCALâ„¢"
+html_logo = "../_static/img/ecal-logo.svg"
+html_favicon = "../_static/img/favicon.png"
+
+html_theme_options = {
+ "logo_only": True,
+ "show_navbar_depth": 1,
+ "show_toc_level": 2,
+ "repository_url": "https://github.com/eclipse-ecal/ecal/",
+ "use_repository_button": False,
+ "use_issues_button": False,
+ "use_edit_page_button": False,
+ "repository_branch": "master",
+ "path_to_docs": "doc/rst/",
+ "extra_navbar": "", # => Remove the default text
+ "footer_start": ["footer.html"],
+ "extra_footer": '',
+}
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'eCALdoc'
+
+# C++ defines used in function definitions
+cpp_id_attributes = ['ECAL_API', 'ECALTIME_API']
diff --git a/doc/release_page/generate_release_documentation/__init__.py b/doc/release_page/generate_release_documentation/__init__.py
new file mode 100644
index 0000000000..48ca6a4a23
--- /dev/null
+++ b/doc/release_page/generate_release_documentation/__init__.py
@@ -0,0 +1,431 @@
+import os
+
+import sys
+import re
+import github
+import jinja2
+import semantic_version
+from collections import OrderedDict
+
+ubuntu_default_python_version_dict = \
+{
+ semantic_version.Version("18.4.0"): semantic_version.Version("3.6.0"),
+ semantic_version.Version("20.4.0"): semantic_version.Version("3.8.0"),
+ semantic_version.Version("22.4.0"): semantic_version.Version("3.10.0"),
+ semantic_version.Version("24.4.0"): semantic_version.Version("3.12.0"),
+}
+
+ubuntu_codename_dict = \
+{
+ "noble": semantic_version.Version("24.4.0"),
+ "jammy": semantic_version.Version("22.4.0"),
+ "focal": semantic_version.Version("20.4.0"),
+ "bionic": semantic_version.Version("18.4.0"),
+ "xenial": semantic_version.Version("16.4.0"),
+ "trusty": semantic_version.Version("14.4.0"),
+}
+
+"""
+Retrieves a dictionary of release branches and their corresponding releases from the GitHub repository.
+Args:
+ gh_repo (github.Github): An authenticated GitHub instance.
+Returns:
+ dict: A dictionary where the keys are minor relase numbers (-> patch level
+ set to 0) and Values are Dictionaries of
+ {specific_release : gh_release_object}
+Example:
+ {
+ Version('5.8.0'): {
+ Version('5.8.0'): ,
+ Version('5.8.1'):
+ },
+ Version('5.9.0'): {
+ Version('5.9.0'):
+ }
+ }
+"""
+def get_releases_dict(gh_repo):
+ gh_releases = gh_repo.get_releases()
+
+ gh_release_branches_dict = OrderedDict()
+
+ for gh_release in gh_releases:
+ if gh_release.prerelease or gh_release.draft:
+ continue
+
+ version_string = gh_release.tag_name
+
+ if version_string.startswith("v") or version_string.startswith("V"):
+ version_string = version_string[1:]
+ if version_string.startswith("."):
+ version_string = version_string[1:]
+
+ # Fix format, so it can be parsed by semantic_version:
+ dot_components = version_string.split(".")
+ if len(dot_components) == 4:
+ version_string = '.'.join(dot_components[:-1]) + "+" + dot_components[3]
+ elif len(dot_components) == 5:
+ version_string = '.'.join(dot_components[:-2]) + "-" + dot_components[3] + "+" + dot_components[4]
+
+ try:
+ version = semantic_version.Version(version_string)
+ except:
+ sys.stderr.write("Warning: eCAL Release \"" + gh_release.tag_name + "\" is not parsable to a proper version.\n")
+ continue
+
+ version = semantic_version.Version(version_string)
+ release_branch = semantic_version.Version(major = version.major, minor = version.minor, patch = 0)
+
+ if not release_branch in gh_release_branches_dict:
+ # Initialize dicitonary for this branch
+ gh_release_branches_dict[release_branch] = OrderedDict()
+
+ gh_release_branches_dict[release_branch][version] = gh_release
+
+ # Sort the minor eCAL Release branches by version
+ gh_release_branches_dict = OrderedDict(sorted(gh_release_branches_dict.items(), key = lambda x: x[0], reverse = True))
+
+ # Sort the specific releases by version
+ for release_branch in gh_release_branches_dict:
+ gh_release_branches_dict[release_branch] = OrderedDict(sorted(gh_release_branches_dict[release_branch].items(), key = lambda x: x[0], reverse = True))
+
+ return gh_release_branches_dict
+
+"""
+Retrieves the properties of a given GitHub asset.
+
+Args:
+ ecal_version (semantic_version.Version): The version of eCAL.
+ gh_asset (github.Asset): The GitHub asset object.
+
+Returns:
+ dict: A dictionary containing the properties of the asset, including:
+
+ {
+ 'filename' : 'actual filename',
+ 'download_link': 'browser download link',
+ 'type' : 'source / ecal_installer / python_binding',
+ 'properties' : SEE BELOW,
+ }
+
+ Properties for "source":
+ {}
+
+ Properties for "ecal_installer":
+ {
+ 'os': 'windows / macos / ubuntu',
+ 'os_version': Semver('0.0.0') (Only for Ubuntu specific installers),
+ 'cpu': 'amd64 / arm64'
+ }
+
+ Properties for "python_binding":
+ {
+ 'os': 'manylinux / macos / windows / ubuntu',
+ 'os_version': Semver('0.0.0') (Only for Ubuntu specific bindings),
+ 'cpu': 'amd64 / arm64',
+ 'python_version': Semver('3.6.0') (example)
+ 'python_implementation': 'cp / pp etc.' (i.e. CPython or PyPy)
+ }
+"""
+def get_asset_properties(ecal_version, gh_asset):
+ asset_properties = {
+ 'filename' : '',
+ 'download_link': '',
+ 'type' : '',
+ 'properties' : {},
+ }
+
+ asset_properties['filename'] = gh_asset.name
+ asset_properties['download_link'] = gh_asset.browser_download_url
+
+ # Source
+ if asset_properties['filename'].endswith('tar.gz'):
+ asset_properties['type'] = 'source'
+
+ # eCAL Installer for Windows
+ elif asset_properties['filename'].endswith('.msi') or asset_properties['filename'].endswith('.exe'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'windows'
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # eCAL installer for macOS
+ elif asset_properties['filename'].endswith('.dmg'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'macos'
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # eCAL Installer for Linux
+ elif asset_properties['filename'].endswith('.deb'):
+ asset_properties['type'] = 'ecal_installer'
+ asset_properties['properties']['os'] = 'ubuntu'
+ asset_properties['properties']['os_version'] = semantic_version.Version('0.0.0')
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+ else:
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+
+ # Python binding (whl)
+ elif asset_properties['filename'].endswith('.whl'):
+ asset_properties['type'] = 'python_binding'
+
+ # Get Operating system
+ if 'manylinux' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'manylinux'
+ elif 'darwin' in asset_properties['filename'] or 'macos' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'macos'
+ elif "win64" in asset_properties['filename'] or "win_amd64" in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'windows'
+ elif 'linux' in asset_properties['filename']:
+ # Old eCAL 5.x wheels were Ubuntu specific
+ asset_properties['properties']['os'] = 'ubuntu'
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+ else:
+ sys.stderr.write("Warning: Unable to determine OS of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get Python CPU Architecture
+ filename_without_extension = os.path.splitext(asset_properties['filename'])[0]
+ if filename_without_extension.endswith('amd64') or filename_without_extension.endswith('x86_64') or filename_without_extension.endswith('win64'):
+ asset_properties['properties']['cpu'] = 'amd64'
+ elif filename_without_extension.endswith('arm64') or filename_without_extension.endswith('aarch64'):
+ asset_properties['properties']['cpu'] = 'arm64'
+ else:
+ sys.stderr.write("Warning: Unable to determine CPU Architecture of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get Python version
+ python_version = semantic_version.Version("0.0.0")
+ python_implementation = ''
+ components = asset_properties['filename'][:-4].split('-')
+ # The python version is either index 2 or 3, depending on whether the optional build tag is used.
+ for index in range(2,4):
+ if re.match(r"[a-z]{2}[0-9]+", components[index]):
+ python_implementation = components[index][:2]
+ python_version_major_string = components[index][2]
+ python_verstion_minor_string = "0"
+ if len(components[index]) > 3:
+ python_verstion_minor_string = components[index][3:]
+ python_version = semantic_version.Version(python_version_major_string + "." + python_verstion_minor_string + ".0")
+ break
+
+ if python_version == semantic_version.Version("0.0.0"):
+ sys.stderr.write("Warning: Unable to determine python version: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+ if not python_implementation:
+ sys.stderr.write("Warning: Unable to determine python implementation: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ asset_properties['properties']['python_version'] = python_version
+ asset_properties['properties']['python_implementation'] = python_implementation
+
+ # Python binding (.egg)
+ elif asset_properties['filename'].endswith('.egg'):
+ asset_properties['type'] = 'python_binding'
+
+ # Get Operating system
+ if 'darwin' in asset_properties['filename'] or 'macos' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'macos'
+ elif "win64" in asset_properties['filename'] or "win_amd64" in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'windows'
+ elif 'linux' in asset_properties['filename'] or 'bionic' in asset_properties['filename'] or 'focal' in asset_properties['filename']:
+ asset_properties['properties']['os'] = 'ubuntu'
+ if ecal_version <= semantic_version.Version("5.7.2"):
+ # Special case for old releases. They only had Ubuntu 20.04 releases that were just named "linux".
+ asset_properties['properties']['os_version'] = semantic_version.Version("20.4.0")
+ else:
+ for codename, version in ubuntu_codename_dict.items():
+ if codename in asset_properties['filename']:
+ asset_properties['properties']['os_version'] = version
+ break
+
+ if not asset_properties['properties'].get('os'):
+ sys.stderr.write("Warning: Unable to determine OS of python binding: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ # Get python version
+ python_version = semantic_version.Version("0.0.0")
+ python_match_result = re.findall(r"py[0-9]+\.[0-9]+", asset_properties['filename'])
+ if len(python_match_result) > 0:
+ python_version = semantic_version.Version(python_match_result[0][2:] + ".0")
+
+ if python_version == semantic_version.Version("0.0.0"):
+ sys.stderr.write("Warning: Unable to determine python version: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ asset_properties['properties']['python_version'] = python_version
+ asset_properties['properties']['python_implementation'] = 'cp' # The eggs were all CPython
+
+ # CPU Architecture was always amd64 back then
+ asset_properties['properties']['cpu'] = 'amd64'
+
+ # Warning, as we have no idea what this file is for
+ else:
+ sys.stderr.write("Warning: Unknown asset type: \"" + asset_properties['filename'] + "\" (from eCAL " + str(ecal_version) + ")\n")
+
+ return asset_properties
+
+def generate_release_index_page(releases_dict, list_of_supported_minor_versions, output_filename):
+ # Create the output directory if it does not exist
+ output_dir = os.path.dirname(output_filename)
+ if output_dir and not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Load the Jinja2 template
+ template_loader = jinja2.FileSystemLoader(searchpath="resource/")
+ template_env = jinja2.Environment(loader=template_loader)
+ template_file = "release_page_index.rst.jinja"
+ template = template_env.get_template(template_file)
+
+ # Render the template with the context
+ context = {
+ 'releases_dict': releases_dict,
+ 'list_of_supported_minor_versions': list_of_supported_minor_versions,
+ 'group_asset_list_by_os_and_arch': group_asset_list_by_os_and_arch,
+ 'get_rst_release_page_label': get_rst_release_page_label,
+ }
+ output = template.render(context)
+
+ # Save the rendered template to a file
+ with open(output_filename, "w") as f:
+ f.write(output)
+
+def generate_release_page(gh_release,
+ ecal_version,
+ asset_list,
+ minor_is_supported,
+ latest_version_of_minor,
+ output_dir):
+ ecal_version_string = str(ecal_version).replace('.', '_').replace('-', '_').replace('+', '_')
+ output_filename = "ecal_" + ecal_version_string + ".rst"
+ changelog_file = "changelog_ecal_" + ecal_version_string + ".txt"
+
+ # Create the output directory if it does not exist
+ if output_dir and not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Save the changelog to a file
+ changelog = gh_release.body
+ changelog = changelog.replace('\r\n', '\n')
+ with open(os.path.join(output_dir, changelog_file), "w") as f:
+ f.write(changelog)
+
+ # Load the Jinja2 template
+ template_loader = jinja2.FileSystemLoader(searchpath="resource/")
+ template_env = jinja2.Environment(loader=template_loader)
+ template_file = "release_page.rst.jinja"
+ template = template_env.get_template(template_file)
+
+ # Render the template with the context
+ context = {
+ 'gh_release': gh_release,
+ 'ecal_version': ecal_version,
+ 'asset_list': asset_list,
+ 'changelog_file_path': changelog_file,
+ 'minor_is_supported': minor_is_supported,
+ 'latest_version_of_minor': latest_version_of_minor,
+ 'ubuntu_default_python_version_dict': ubuntu_default_python_version_dict,
+ 'group_asset_list_by_os_and_arch': group_asset_list_by_os_and_arch,
+ 'get_rst_release_page_label': get_rst_release_page_label,
+ }
+ output = template.render(context)
+
+ # Save the rendered template to a file
+ path = os.path.join(output_dir, output_filename)
+ with open(path, "w") as f:
+ f.write(output)
+
+"""
+Groups a list of assets by their OS, OS version, and CPU architecture.
+Args:
+ asset_list (list): A list of asset properties dictionaries.
+Returns:
+ dict: A dictionary where the keys are tuples of (os, os_version, cpu) and
+ the values are lists of asset properties dictionaries.
+"""
+def group_asset_list_by_os_and_arch(asset_list):
+ os_arch_dict = OrderedDict()
+
+ for asset_properties in asset_list:
+ os = asset_properties['properties'].get('os', 'unknown')
+ os_version = asset_properties['properties'].get('os_version', semantic_version.Version('0.0.0'))
+ cpu = asset_properties['properties'].get('cpu', 'unknown')
+
+ key = (os, os_version, cpu)
+ if key not in os_arch_dict:
+ os_arch_dict[key] = []
+ os_arch_dict[key].append(asset_properties)
+
+ return os_arch_dict
+
+def get_rst_release_page_label(ecal_version):
+ return "ecal_release_page_" + str(ecal_version).replace('.', '_').replace('-', '_').replace('+', '_')
+
+def generate_release_documentation(gh_api_key, index_filepath, release_dir_path):
+ gh = github.Github(gh_api_key)
+ gh_repo = gh.get_repo("eclipse-ecal/ecal")
+ releases_dict = get_releases_dict(gh_repo)
+
+ list_of_supported_minor_versions = list(releases_dict.keys())[:2]
+
+ generate_release_index_page(releases_dict,
+ list_of_supported_minor_versions,
+ index_filepath)
+
+ for minor_version in releases_dict:
+
+ # Get the latest eCAL Version of this release branch
+ latest_release_for_this_minor = list(releases_dict[minor_version].keys())[0]
+ this_minor_is_supported = minor_version in list_of_supported_minor_versions
+
+ for ecal_version in releases_dict[minor_version]:
+
+ # Check the support status of this release
+
+ gh_release = releases_dict[minor_version][ecal_version]
+ gh_asset_list = gh_release.get_assets()
+
+ asset_list = []
+
+ for gh_asset in gh_asset_list:
+ asset_properties = get_asset_properties(ecal_version, gh_asset)
+ asset_list.append(asset_properties)
+
+ # Sort the asset list by:
+ # 1. OS (windows > manylinux > ubuntu > macos)
+ # 2. OS version (descending)
+ # 3. CPU (amd64 > arm64)
+ # 4. Python version (descending)
+
+ asset_list.sort(key = lambda x: x['properties'].get('python_version', semantic_version.Version("0.0.0")), reverse = True)
+ asset_list.sort(key = lambda x: x['properties'].get('os_version', semantic_version.Version("0.0.0")), reverse = True)
+ asset_list.sort(key = lambda x: x['properties'].get('cpu', 'unknown'))
+ asset_list.sort(key = lambda x: ['windows', 'manylinux', 'ubuntu', 'macos'].index(x['properties'].get('os', 'unknown')) if x['properties'].get('os', 'unknown') in ['windows', 'manylinux', 'ubuntu', 'macos'] else float('inf'))
+
+ generate_release_page(gh_release,
+ ecal_version,
+ asset_list,
+ this_minor_is_supported,
+ latest_release_for_this_minor,
+ release_dir_path)
+
+if __name__=="__main__":
+ gh_api_key = os.getenv("ECAL_GH_API_KEY")
+ if gh_api_key:
+ generate_release_documentation(gh_api_key, "index.rst", "release")
+ else:
+ sys.stderr.write("ERROR: Environment variable ECAL_GH_API_KEY not set. Without an API key, GitHub will not provide enough API calls to generate the download tables.\n")
+ exit(1)
+
+
+
+
+
+
+
diff --git a/doc/release_page/resource/release_page.rst.jinja b/doc/release_page/resource/release_page.rst.jinja
new file mode 100644
index 0000000000..4411a39b01
--- /dev/null
+++ b/doc/release_page/resource/release_page.rst.jinja
@@ -0,0 +1,206 @@
+{% set ecal_version_string = ecal_version | string -%}
+
+{# Makro for properly naming the OS, possibly with icon. e.g. "|fa-windows| Windows" (-> capitalized) or |fa-ubuntu| Ubuntu 24.04 (-> with Version number) -#}
+{% macro get_os_string(asset, with_icon) -%}
+ {% if asset['properties']['os'] == 'windows' -%}
+ {% set os_string = (with_icon and '|fa-windows| ' or '') ~ 'Windows' -%}
+ {% elif asset['properties']['os'] == 'macos' -%}
+ {% set os_string = (with_icon and '|fa-apple| ' or '') ~ 'macOS' -%}
+ {% elif asset['properties']['os'] == 'ubuntu' -%}
+ {% set os_version_padded = asset['properties']['os_version'].major ~ '.' ~ '%02d' | format(asset['properties']['os_version'].minor) -%}
+ {% set os_string = (with_icon and '|fa-Ubuntu| ' or '') ~ 'Ubuntu ' ~ os_version_padded -%}
+ {% elif asset['properties']['os'] == 'manylinux' -%}
+ {% set os_string = (with_icon and '|fa-linux| ' or '') ~ 'Linux (All)' -%}
+ {% elif asset['properties']['os'] == '' -%}
+ {% set os_string = 'Unknown' -%}
+ {% else -%}
+ {% set os_string = asset['properties']['os'].capitalize() -%}
+ {% endif -%}
+ {{ os_string -}}
+{% endmacro -%}
+
+{# Makro for naming the CPU Architecture. Mainly to make amd64 to x64, so the user can more easily distinguish it from arm64 #}
+{% macro get_cpu_string(asset) -%}
+ {% if asset['properties']['cpu'] == 'amd64' -%}
+ {% set cpu_string = 'x64' -%}
+ {% elif asset['properties']['cpu'] == 'arm64' -%}
+ {% set cpu_string = 'ARM64' -%}
+ {% else -%}
+ {% set cpu_string = asset['properties']['cpu'] -%}
+ {% endif -%}
+ {{ cpu_string -}}
+{% endmacro -%}
+:orphan:
+
+.. include:: /../include.txt
+
+.. _{{ get_rst_release_page_label(ecal_version) }}:
+
+===============
+eCAL {{ ecal_version_string }}
+===============
+
+{% if not minor_is_supported %}
+.. warning::
+
+ eCAL {{ ecal_version.major }}.{{ ecal_version.minor }} is not supported anymore. Please consider upgrading to a newer version.
+{% endif %}
+
+- Release Date: {{ gh_release.published_at.strftime('%Y-%m-%d') }}
+- GitHub Release Page: {{ gh_release.html_url }}
+
+Changelog
+=========
+
+.. literalinclude:: {{ changelog_file_path }}
+ :language: text
+
+Downloads
+=========
+
+{% if ecal_version < latest_version_of_minor %}
+.. note::
+
+ A more recent version of eCAL {{ ecal_version.major }}.{{ ecal_version.minor }} is available: :ref:`eCAL {{ latest_version_of_minor }} <{{ get_rst_release_page_label(latest_version_of_minor) }}>`.
+{% endif %}
+
+{# Get the ecal_installers from the list and remove them from the main list. -#}
+{% set ecal_installer_list = asset_list | selectattr('type', 'equalto', 'ecal_installer') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'ecal_installer') | list -%}
+
+{% if ecal_installer_list %}
+eCAL Installer
+--------------
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% for asset in ecal_installer_list -%}
+
+ * - {{ get_os_string(asset, true) }}
+
+ - {{ get_cpu_string(asset) }}
+
+ - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the python bindings from the list -#}
+{% set python_binding_list = asset_list | selectattr('type', 'equalto', 'python_binding') | selectattr('properties.python_implementation', 'equalto', 'cp') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'python_binding') | list -%}
+
+{% if python_binding_list %}
+|fa-python| Python Binding
+--------------------------
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% set python_binding_grouped_dict = group_asset_list_by_os_and_arch(python_binding_list) -%}
+
+ {% for key, value in python_binding_grouped_dict.items() -%}
+ {% if value[0]['properties']['os'] == 'ubuntu' -%}
+ {% set default_python_version = ubuntu_default_python_version_dict[value[0]['properties']['os_version']] -%}
+ {% endif %}
+ * - {{ get_os_string(value[0], true) }}
+
+ - {{ get_cpu_string(value[0]) }}
+
+ - {% for asset in value -%}
+ `Python {{ asset['properties']['python_version'].major }}.{{ asset['properties']['python_version'].minor }} (.{{ asset['filename'].split('.')[-1] }}) <{{ asset['download_link'] }}>`__ {% if asset['properties']['python_version'] == default_python_version %} (Default){% endif %}
+
+ {% endfor -%}
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the PyPy bindings from the list -#}
+{% set pypy_binding_list = asset_list | selectattr('type', 'equalto', 'python_binding') | selectattr('properties.python_implementation', 'equalto', 'pp') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'python_binding') | list -%}
+
+{% if pypy_binding_list %}
+|fa-python| PyPy Binding
+------------------------
+
+*PyPy bindings are not compatibly with the regular Python implementation.*
+
+.. list-table::
+ :widths: 2 2 6
+ :header-rows: 1
+
+ * - OS
+
+ - Architecture
+
+ - Files
+
+ {% set pypy_binding_grouped_dict = group_asset_list_by_os_and_arch(pypy_binding_list) -%}
+
+ {% for key, value in pypy_binding_grouped_dict.items() %}
+ * - {{ get_os_string(value[0], true) }}
+
+ - {{ get_cpu_string(value[0]) }}
+
+ - {% for asset in value -%}
+ `PyPy {{ asset['properties']['python_version'].major }}.{{ asset['properties']['python_version'].minor }} (.{{ asset['filename'].split('.')[-1] }}) <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+ {% endfor -%}
+
+{% endif -%}
+
+{# Get the source files from the list -#}
+{% set source_file_list = asset_list | selectattr('type', 'equalto', 'source') | list -%}
+{% set asset_list = asset_list | rejectattr('type', 'equalto', 'source') | list -%}
+
+{% if source_file_list %}
+|fa-code| Source
+----------------
+
+.. list-table::
+ :widths: 1
+
+ {% for asset in source_file_list -%}
+
+ * - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif %}
+
+{# Get the other files from the list -#}
+{% if asset_list %}
+Other Downloads
+---------------
+
+.. list-table::
+ :widths: 1
+ :header-rows: 1
+
+ * - Files
+
+ {% for asset in asset_list -%}
+
+ * - `{{ asset['filename'] }} <{{ asset['download_link'] }}>`__
+
+ {% endfor -%}
+
+{% endif -%}
\ No newline at end of file
diff --git a/doc/release_page/resource/release_page_index.rst.jinja b/doc/release_page/resource/release_page_index.rst.jinja
new file mode 100644
index 0000000000..30f6c6d4bc
--- /dev/null
+++ b/doc/release_page/resource/release_page_index.rst.jinja
@@ -0,0 +1,49 @@
+.. include:: /../include.txt
+
+.. _all_releases:
+
+=============
+eCAL Releases
+=============
+
+Here you can find a list of all eCAL Versions ever released on GitHub.
+
+{% for minor_version, releases_dict in releases_dict.items() %}
+eCAL {{ minor_version.major }}.{{ minor_version.minor }}
+===========
+
+{% if minor_version not in list_of_supported_minor_versions -%}
+
+*eCAL {{ minor_version.major }}.{{ minor_version.minor }} has reached its end of life.*
+
+{% endif -%}
+
+.. list-table::
+ :widths: 3 3 4
+ :header-rows: 1
+
+ * - Release
+
+ - Release Date
+
+ - Support Status
+
+{% for ecal_version, gh_release in releases_dict.items() -%}
+{% set is_latest_release_in_this_minor = (ecal_version == releases_dict.keys()|list|first) %}
+ * - :ref:`eCAL {{ gh_release.tag_name }} <{{ get_rst_release_page_label(ecal_version) }}>`
+
+ - {{ gh_release.published_at.strftime("%Y-%m-%d") }}
+
+ - {% if (minor_version in list_of_supported_minor_versions) and is_latest_release_in_this_minor -%}
+ Supported
+ {% else -%}
+ EOL
+ {% endif -%}
+{% endfor -%}
+
+{% endfor %}
+
+.. toctree::
+ :hidden:
+
+ eCAL Documentation
\ No newline at end of file