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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_static/img/favicon.png b/doc/_static/img/favicon.png new file mode 100644 index 0000000000..fe298c679a Binary files /dev/null and b/doc/_static/img/favicon.png differ diff --git a/doc/_templates/footer.html b/doc/_templates/footer.html new file mode 100644 index 0000000000..447ca399ff --- /dev/null +++ b/doc/_templates/footer.html @@ -0,0 +1,26 @@ + + + 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