diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..0dc4c3f88e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,97 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Deploy Website and Docs + +on: + pull_request: + push: + branches: + - master + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build_website: + runs-on: ubuntu-latest + steps: + # Build website from gazebosim-web-frontend + - name: Checkout + uses: actions/checkout@v4 + with: + repository: gazebo-web/gazebosim-web-frontend + ref: main + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: package-lock.json + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Install Website dependencies + run: npm ci + - name: Build Website + run: npm run build -- --base-href "${{ steps.pages.outputs.base_url }}/" + # Upload the artifact for local preview + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: website + path: dist + + # Build Docs + build_docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install Docs dependencies + run: pip install -r requirements.txt + - name: Build Docs + run: python build_multiversion.py --pointers --libs --output_dir .build + env: + GZ_DEPLOY_URL: "${{ steps.pages.outputs.base_url }}" + + # Upload the artifact for local preview + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: .build + + deploy: + runs-on: ubuntu-latest + needs: [build_website, build_docs] + permissions: + contents: write + # Allow only one concurrent deployment between this and the nightly-upload workflow. + concurrency: + group: pages + cancel-in-progress: false + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Upload merged + uses: actions/upload-artifact@v4 + with: + name: website-docs-merged + path: ./ + - name: Commit + uses: peaceiris/actions-gh-pages@v4 + # The workflow upto this point is good for generating a preview, + # but only commit to deploy if we are on the master branch (not a pull request). + if: github.ref == 'refs/heads/master' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./ + keep_files: true diff --git a/.github/workflows/nightly-upload.yml b/.github/workflows/nightly-upload.yml index cd80dfcfa5..0f9e92cdca 100644 --- a/.github/workflows/nightly-upload.yml +++ b/.github/workflows/nightly-upload.yml @@ -58,17 +58,14 @@ jobs: needs: build runs-on: ubuntu-latest permissions: - id-token: write - contents: read + contents: write + # Allow only one concurrent deployment between this and the deploy workflow. + concurrency: + group: pages + cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@v4 - - name: Configure AWS Credentials - id: creds - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-1 - role-to-assume: arn:aws:iam::200670743174:role/github-oidc-deployment-gz-web-app - uses: actions/download-artifact@v4 id: download with: @@ -81,8 +78,10 @@ jobs: with: name: api-docs path: .api-out/* - - name: Run nightly upload - run: aws s3 sync .api-out/ s3://gazebosim.org/api/ - - name: Invalidate Cloudfront distribution - run: | - aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths '/*' --region us-east-1 + - name: Commit + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./.api-out + destination_dir: api + keep_files: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..7afd5d306b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.build +.tmp +.venv diff --git a/README.md b/README.md index 0761587011..731656c61b 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,45 @@ found under the `API Reference` section of [https://gazebosim.org/docs](https:// ## Main docs -The documentation in this repository is updated whenever the -[gazebosim-web-backend](https://github.com/gazebo-web/gazebosim-web-backend), -is deployed. The gazebosim-web-backend webserver maintains a clone of this repository, and serves the markdown pages to https://gazebosim.org/docs. +The documentation in this repository is built using [Sphinx](https://www.sphinx-doc.org/). +To build, you need to install the following: + +* python virtualenv + +Create the virtual env and activate it: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Then install the necessary dependencies: + +```bash +pip install -r requirements.txt +``` + +```bash +python3 build_multiversion.py +``` + +This will build all the documentation for all versions of Gazebo. +You can preview the result locally by running an HTTP server on +the output directory `.build`. For example: + +```bash +python3 -m http.server 8000 -d .build + +``` + +This will serve the website on + +For quicker iteration, you can build the documentation for a subset +of Gazebo versions. To build `garden` and `harmonic`: + +```bash +python3 build_multiversion.py --release garden harmonic +``` ## Library docs diff --git a/_static/css/gazebo.css b/_static/css/gazebo.css new file mode 100644 index 0000000000..aa9cd46bcd --- /dev/null +++ b/_static/css/gazebo.css @@ -0,0 +1,149 @@ +html { + --pst-font-family-base: Roboto, var(--pst-font-family-base-system); + --pst-font-size-base: 14px; + --pst-header-height: 64px; + --gz-doc-header-height: 120px; + scroll-padding-top: calc(var(--pst-header-height) + var(--gz-doc-header-height) + 1rem); +} + +html[data-theme="light"] { + --gz-color-doc-header: #4fc3f7; + --gz-color-doc-header-text: #fff; + --pst-color-primary: #0277bd; + --gz-color-primary-sidebar: #f8f9fa; +} + +html[data-theme="dark"] { + --gz-color-doc-header: rgb(15 23 36 / 30%); + --gz-color-doc-header-text: #fff; + --pst-color-primary: #0277bd; + --gz-color-primary-sidebar: #1a1c1e; + --pst-color-background: #131416; +} + +a { + text-decoration: none; +} + +pre { + border: none; +} +.bd-main .bd-content .bd-article-container { + max-width: 160em; +} + +.bd-page-width { + max-width: 100%; +} + +.bd-sidebar { + max-width: 20em; +} + +.sidebar-primary-items__end { + display: none; +} + +.bd-links__title { + display: none; +} + +.navbar-nav .nav-item { + letter-spacing: normal; + text-transform: uppercase; + font-size: 16px; + text-decoration: none; + outline: 0; + transition: 0.5s; + font-weight: 400; + color: #6e6e6e; + border-bottom: 1px solid rgba(0, 0, 0, 0); +} +.navbar-nav li a { + margin-right: 20px; +} + +.bd-header .navbar-header-items { + padding-left: 5em; +} + +.nav-link.nav-external:after { + display: none; +} + +.doc-header { + width: 100%; + background-color: var(--gz-color-doc-header); + color: var(--gz-color-doc-header-text); + height: var(--gz-doc-header-height); +} + +.banner { + align-items: center; + padding: 20px 40px; + place-content: center space-between; + height: 100%; +} + +header.navbar { + display: block; +} + +.bd-sidebar-primary { + max-height: calc(100vh - var(--pst-header-height) + var(--gz-doc-header-height)); + top: calc(var(--pst-header-height) + var(--gz-doc-header-height)); + background-color: var(--gz-color-primary-sidebar); +} + +.bd-sidebar-secondary { + max-height: calc(100vh - var(--pst-header-height) + var(--gz-doc-header-height)); + top: calc(var(--pst-header-height) + var(--gz-doc-header-height)); +} + +.gz-version-switcher { + display: flex; +} +button.btn.version-switcher__button { + color: var(--gz-color-doc-header-text); + border-color: var(--gz-color-doc-header-text); +} +.bd-header-version-info { + background-color: var(--pst-color-info-bg); +} + +.navbar-brand img { + height: 84px; +} + +.warning { + border-left: 3px solid rgb(228, 167, 2); + padding: 15px; + margin: 15px 0; + color: #8a6c40; + background: rgb(252, 248, 228); +} + +/* Style for /libs */ +.gz-libs-lists { + display: inline; + padding: 0; +} + +.gz-libs-lists p { + display: inline; +} + +.gz-libs-lists li, .gz-libs-cards li { + list-style-type: none; + display: inline; + padding-left: 0px; + padding-right: 30px; +} + +.gz-libs-cards ul { + padding-left: 1em; +} + +.gz-libs-cards .sd-card-text { + display: inline; +} diff --git a/_static/icon/favicon.ico b/_static/icon/favicon.ico new file mode 100644 index 0000000000..3979b1cf68 Binary files /dev/null and b/_static/icon/favicon.ico differ diff --git a/_static/images/logos/gazebo_horz_neg.svg b/_static/images/logos/gazebo_horz_neg.svg new file mode 100644 index 0000000000..1b0aaab8d8 --- /dev/null +++ b/_static/images/logos/gazebo_horz_neg.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/_static/images/logos/gazebo_horz_pos.svg b/_static/images/logos/gazebo_horz_pos.svg new file mode 100644 index 0000000000..c3f296ee8e --- /dev/null +++ b/_static/images/logos/gazebo_horz_pos.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_templates/fuel_app_link.html b/_templates/fuel_app_link.html new file mode 100644 index 0000000000..5e9c34e13e --- /dev/null +++ b/_templates/fuel_app_link.html @@ -0,0 +1 @@ +APP diff --git a/_templates/gz-footer.html b/_templates/gz-footer.html new file mode 100644 index 0000000000..1af95b7893 --- /dev/null +++ b/_templates/gz-footer.html @@ -0,0 +1,4 @@ +

Brought to you by Open Robotics.

+

Except where otherwise noted, the gazebosim.org web pages are licensed under Creative Commons + Attribution 3.0.

diff --git a/_templates/gz-navbar-nav.html b/_templates/gz-navbar-nav.html new file mode 100644 index 0000000000..c52a58fbb1 --- /dev/null +++ b/_templates/gz-navbar-nav.html @@ -0,0 +1,19 @@ + + diff --git a/_templates/gz-sidebar-nav.html b/_templates/gz-sidebar-nav.html new file mode 100644 index 0000000000..db9b8f866b --- /dev/null +++ b/_templates/gz-sidebar-nav.html @@ -0,0 +1,16 @@ +{# Temlate to generate sidedbar navigation from level 0 toc items #} + diff --git a/_templates/gz-version-switcher.html b/_templates/gz-version-switcher.html new file mode 100644 index 0000000000..5808190226 --- /dev/null +++ b/_templates/gz-version-switcher.html @@ -0,0 +1,4 @@ +
+ Release: +{%- include 'version-switcher.html' -%} +
diff --git a/_templates/sections/header.html b/_templates/sections/header.html new file mode 100644 index 0000000000..d046f3e16d --- /dev/null +++ b/_templates/sections/header.html @@ -0,0 +1,52 @@ +{% if release_info %} + {% if not release_info.preferred %} + + {% endif %} +{% endif %} + +{% include "!sections/header.html" %} + +{% if release_info %} +
+ +
+{% else %} +{# Assume /libs #} +
+ +
+{% endif %} diff --git a/base_conf.py b/base_conf.py new file mode 100644 index 0000000000..b29260d9db --- /dev/null +++ b/base_conf.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os + +project = "Gazebo" +copyright = "2024, Open Robotics" +author = "Gazebo Team" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "myst_parser", + "sphinx_copybutton", + "sphinx_design", + # 'sphinx_sitemap', +] + +templates_path = ["_templates"] + +source_suffix = [ + ".md", +] + +myst_heading_anchors = 4 +myst_all_links_external = True + +myst_enable_extensions = [ + "amsmath", + "attrs_inline", + "attrs_block", + "colon_fence", + "deflist", + "dollarmath", + "fieldlist", + "html_admonition", + "html_image", + "linkify", + "replacements", + "smartquotes", + "strikethrough", + "substitution", + "tasklist", +] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "pydata_sphinx_theme" +html_static_path = ["_static"] +html_css_files = ["css/gazebo.css"] +html_favicon = "_static/icon/favicon.ico" + +html_theme_options = { + "header_links_before_dropdown": 4, + "use_edit_page_button": True, + "show_toc_level": 1, + "navigation_with_keys": False, + "show_prev_next": False, + "footer_center": ["gz-footer"], + "footer_start": ["sphinx-version"], + "secondary_sidebar_items": ["page-toc", "edit-this-page"], + "navbar_align": "left", + "navbar_center": ["gz-navbar-nav"], + "navbar_end": ["navbar-icon-links", "theme-switcher", "fuel_app_link"], + "pygments_light_style": "tango", + "pygments_dark_style": "monokai", + "logo": { + "image_light": "_static/images/logos/gazebo_horz_pos.svg", + "image_dark": "_static/images/logos/gazebo_horz_neg.svg", + }, + "check_switcher": False, + # We have our own version, so we disable the one from the theme. + "show_version_warning_banner": False, +} + +html_sidebars = {"**": ["gz-sidebar-nav"]} + +html_context = { + "deploy_url": os.environ.get("GZ_DEPLOY_URL", "") +} diff --git a/blueprint/ros_integration.md b/blueprint/ros_integration.md index be30d83f92..520865f38f 100644 --- a/blueprint/ros_integration.md +++ b/blueprint/ros_integration.md @@ -101,4 +101,4 @@ The screenshot shows all the shell windows and their expected content (it was taken using ROS Melodic): -![Ignition Transport images and ROS 1 rqt](../acropolis/images/bridge_image_exchange_ign-gazebo.png) +![Ignition Transport images and ROS 1 rqt](img/bridge_image_exchange_gz-sim.png) diff --git a/build_multiversion.py b/build_multiversion.py new file mode 100644 index 0000000000..3662bc108c --- /dev/null +++ b/build_multiversion.py @@ -0,0 +1,454 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from string import Template +import argparse +import copy +import json +import os +import requests +import shutil +import sys +import subprocess +import yaml + +additional_shared_directories = ["images", "releasing"] + + +def _combine_nav(common_nav, release_nav): + combined = copy.deepcopy(common_nav) + # Release are added after 'get_started' + for i, item in enumerate(release_nav): + combined.insert(i + 1, item) + return combined + + +def copy_pages(pages, root_src_dir, dst): + for page in pages: + full_dst = Path(dst) / page["file"] + if full_dst.parent != dst: + full_dst.parent.mkdir(parents=True, exist_ok=True) + + shutil.copy2(root_src_dir / page["file"], full_dst) + if "children" in page: + copy_pages(page["children"], root_src_dir, dst) + + +def generate_sources(gz_nav_yaml, root_src_dir, tmp_dir, gz_release): + + if not gz_release: + raise RuntimeError("gz_release not provided") + # Copy release-specific directory + version_src_dir = Path(root_src_dir) / gz_release + + matching_release = [ + release for release in gz_nav_yaml["releases"] if release["name"] == gz_release + ] + if not matching_release: + raise RuntimeError( + f"Provided gz_release '{gz_release}' not registered in `index.yaml`" + ) + elif len(matching_release) > 1: + raise RuntimeError(f"More than one releases named '{gz_release}' found.") + + release_info = matching_release[0] + + tmp_dir.mkdir(exist_ok=True) + version_tmp_dir = tmp_dir / gz_release + + shutil.copytree(version_src_dir, version_tmp_dir, dirs_exist_ok=True) + for dir in ["_static", "_templates"]: + shutil.copytree(root_src_dir / dir, version_tmp_dir / dir, dirs_exist_ok=True) + + shutil.copy2(root_src_dir / "base_conf.py", version_tmp_dir) + shutil.copy2(root_src_dir / "conf.py", version_tmp_dir) + + for dir in additional_shared_directories: + shutil.copytree(root_src_dir / dir, version_tmp_dir / dir, dirs_exist_ok=True) + + copy_pages(gz_nav_yaml["pages"], root_src_dir, version_tmp_dir) + + deploy_url = os.environ.get("GZ_DEPLOY_URL", "") + # Write switcher.json file + switcher = [] + for release in gz_nav_yaml["releases"]: + name = release["name"].capitalize() + if release["eol"]: + name += " (EOL)" + elif release["lts"]: + name += " (LTS)" + elif release.get("dev", False): + name += " (dev)" + + switcher.append( + { + "name": name, + "version": release["name"], + "url": f"{deploy_url}/docs/{release['name']}/", + "preferred": release.get("preferred", False) + } + ) + + static_dir = version_tmp_dir / "_static" + static_dir.mkdir(exist_ok=True) + json.dump(switcher, open(static_dir / "switcher.json", "w")) + + def handle_file_url_rename(file_path, file_url): + computed_url, ext = os.path.splitext(file_path) + # print("renames:", file_path, file_url) + if file_url != computed_url: + new_path = file_url + ext + # If the file url is inside a directory, we want the new path to end up in the same directory + # print("Moving", version_tmp_dir / file_path, version_tmp_dir / new_path) + shutil.move(version_tmp_dir / file_path, version_tmp_dir / new_path) + return new_path + return file_path + + toc_directives = ["{toctree}", ":hidden:", ":maxdepth: 1", ":titlesonly:"] + + with open(version_tmp_dir / "index.yaml") as f: + version_nav_yaml = yaml.safe_load(f) + combined_nav = _combine_nav(gz_nav_yaml["pages"], version_nav_yaml["pages"]) + + nav_md = [] + # TODO(azeey) Make this recursive so multiple levels of + # 'children' can be supported. + for page in combined_nav: + file_url = page["name"] + file_path = page["file"] + + children = page.get("children") + nav_md.append(f"{page['title']} <{page['name']}>") + new_file_path = handle_file_url_rename(file_path, file_url) + + if children: + child_md = [] + for child in children: + file_url = child["name"] + file_path = child["file"] + handle_file_url_rename(file_path, file_url) + child_md.append(f"{child['title']} <{file_url}>") + + with open(version_tmp_dir / new_file_path, "a") as ind_f: + ind_f.write("```") + ind_f.write("\n".join(toc_directives) + "\n") + ind_f.writelines("\n".join(child_md) + "\n") + ind_f.write("```\n") + + library_reference_nav = "library_reference_nav" + libraries = release_info["libraries"] + if libraries: + nav_md.append(library_reference_nav) + # Add Library Reference + with open(version_tmp_dir / f"{library_reference_nav}.md", "w") as ind_f: + ind_f.write("# Library Reference\n\n") + ind_f.write("```") + ind_f.write("{toctree}\n") + for library in libraries: + ind_f.write( + f"{library['name']} \n" + ) + ind_f.write("```\n\n") + + with open(version_tmp_dir / "index.md", "w") as ind_f: + ind_f.write( + """--- +myst: + html_meta: + "http-equiv=refresh": "0; url=getstarted" +--- +""" + ) + ind_f.write("# Index\n\n") + ind_f.write("```") + ind_f.write("\n".join(toc_directives) + "\n") + ind_f.writelines("\n".join(nav_md) + "\n") + ind_f.write("```\n\n") + + +def get_preferred_release(releases: dict): + preferred = [rel for rel in releases if rel.get("preferred", False)] + assert len(preferred) == 1 + return preferred[0] + + +def github_repo_name(lib_name): + prefix = "gz-" if lib_name != "sdformat" else "" + return f"{prefix}{lib_name.replace('_','-')}" + + +def github_branch(repo_name, version): + return f"{repo_name}{version}" if repo_name != "sdformat" else f"sdf{version}" + + +def github_url(lib_name): + return f"https://github.com/gazebosim/{github_repo_name(lib_name)}" + + +def api_url(lib_name, version): + if lib_name == "sdformat": + return "http://sdformat.org/api" + else: + return f"https://gazebosim.org/api/{lib_name}/{version}" + + +def get_github_content(lib_name, version, file_path): + repo_name = github_repo_name(lib_name) + branch = github_branch(repo_name, version) + url = f"https://raw.githubusercontent.com/gazebosim/{repo_name}/{branch}/{file_path}" + if os.environ.get("SKIP_FETCH_CONTENT", False): + return f"Skipped fetching context from {url}" + + print(f"fetching {url}") + result = requests.get(url, allow_redirects=True) + return result.text + + +def generate_individual_lib(library, libs_dir): + lib_name = library["name"] + version = library["version"] + cur_lib_dir = libs_dir / lib_name + cur_lib_dir.mkdir(exist_ok=True) + + template = Template("""\ +# $name + +{.gz-libs-lists} +- [{material-regular}`code;2em` Source Code]($github_url) +- [{material-regular}`description;2em` API & Tutorials]($api_url) + +::::{tab-set} + +:::{tab-item} Readme +$readme +::: + +:::{tab-item} Changelog +$changelog +::: + +:::: + """) + + mapping = { + "name": lib_name, + "readme": get_github_content(lib_name, version, "README.md"), + "changelog": get_github_content(lib_name, version, "Changelog.md"), + "github_url": github_url(lib_name), + "api_url": api_url(lib_name, version), + } + with open(cur_lib_dir / "index.md", "w") as f: + f.write(template.substitute(mapping)) + + +def generate_libs(gz_nav_yaml, libs_dir): + libraries = get_preferred_release(gz_nav_yaml["releases"])["libraries"] + library_directives = "\n".join([ + f"{library['name'].capitalize()} <{library['name']}/index>" + for library in libraries + ]) + + index_md_header_template = Template("""\ +# Libraries + +```{toctree} +:maxdepth: 1 +:hidden: +:titlesonly: +$library_directives +``` + +""") + + library_card_template = Template("""\ +:::{card} [$name_cap]($name/index) +:class-card: gz-libs-cards + - [{material-regular}`fullscreen;2em` Details]($name/index) + - [{material-regular}`code;2em` Source Code]($github_url) + - [{material-regular}`description;2em` API & Tutorials]($api_url) ++++, + +$description +::: + + +""") + with open(libs_dir / "index.md", "w") as f: + f.write( + index_md_header_template.substitute(library_directives=library_directives) + ) + + for library in sorted(libraries, key=lambda lib: lib["name"]): + name = library["name"] + try: + description = gz_nav_yaml["library_info"][name]["description"] + except KeyError as e: + print( + f"Description for library {name} not found." + "Make sure there is an entry for it in index.yaml" + ) + print(e) + description = "" + mapping = { + "name": name, + "name_cap": name.capitalize(), + "github_url": github_url(name), + "api_url": api_url(name, library["version"]), + "description": description, + } + f.write(library_card_template.substitute(mapping)) + + generate_individual_lib(library, libs_dir) + + +def build_libs(gz_nav_yaml, src_dir, tmp_dir, build_dir): + libs_dir = tmp_dir / "libs" + libs_dir.mkdir(exist_ok=True) + shutil.copy2(src_dir / "base_conf.py", libs_dir/"base_conf.py") + shutil.copy2(src_dir / "libs_conf.py", libs_dir/"conf.py") + if len(gz_nav_yaml["releases"]) == 0: + print("No releases found in 'index.yaml'.") + return + + for dir in ["_static", "_templates"]: + shutil.copytree(src_dir / dir, libs_dir / dir, dirs_exist_ok=True) + + build_dir = build_dir / "libs" + + generate_libs(gz_nav_yaml, libs_dir) + + sphinx_args = [ + "sphinx-build", + "-b", + "dirhtml", + f"{libs_dir}", + f"{build_dir}", + ] + subprocess.run(sphinx_args) + + +def main(argv=None): + src_dir = Path(__file__).parent + + # We will assume that this file is in the same directory as documentation + # sources and conf.py files. + parser = argparse.ArgumentParser() + parser.add_argument( + "-r", + "--releases", + metavar="GZ_RELEASES", + nargs="*", + help="Names of releases to build. Builds all known releases if empty.", + ) + parser.add_argument( + "--output_dir", default=src_dir / ".build", help="Path to output directory" + ) + parser.add_argument( + "--libs", action="store_true", default=False, help="Build /libs page" + ) + parser.add_argument( + "--libs_only", action="store_true", default=False, help="Build only /libs page" + ) + parser.add_argument( + "--pointers", + action="store_true", + default=False, + help="Build 'latest' and 'all'", + ) + + args, unknown_args = parser.parse_known_args(argv) + + index_yaml = src_dir / "index.yaml" + assert index_yaml.exists() + + with open(index_yaml) as top_index_file: + gz_nav_yaml = yaml.safe_load(top_index_file) + + if not args.releases: + args.releases = [release["name"] for release in gz_nav_yaml["releases"]] + + preferred_release = get_preferred_release(gz_nav_yaml["releases"]) + tmp_dir = src_dir / ".tmp" + tmp_dir.mkdir(exist_ok=True) + build_dir = Path(args.output_dir) + build_dir.mkdir(exist_ok=True) + if args.libs or args.libs_only: + build_libs(gz_nav_yaml, src_dir, tmp_dir, build_dir) + + if args.libs_only: + return + + build_docs_dir = build_dir / "docs" + for release in args.releases: + generate_sources(gz_nav_yaml, src_dir, tmp_dir, release) + release_build_dir = build_docs_dir / release + sphinx_args = [ + "sphinx-build", + "-b", + "dirhtml", + f"{tmp_dir/release}", + f"{release_build_dir }", + "-D", + f"gz_release={release}", + "-D", + f"gz_root_index_file={index_yaml}", + *unknown_args, + ] + subprocess.run(sphinx_args) + + # Handle "latest" and "all" + release = preferred_release["name"] + if args.pointers and (release in args.releases): + for pointer in ["latest", "all"]: + release_build_dir = build_docs_dir / pointer + pointer_tmp_dir = tmp_dir/pointer + try: + pointer_tmp_dir.symlink_to(tmp_dir/release) + except FileExistsError: + # It's okay for it to exist, but make sure it's a symlink + if not pointer_tmp_dir.is_symlink: + raise RuntimeError( + f"{pointer_tmp_dir} already exists and is not a symlink" + ) + + sphinx_args = [ + "sphinx-build", + "-b", + "dirhtml", + f"{pointer_tmp_dir}", + f"{release_build_dir}", + "-D", + f"gz_release={release}", + "-D", + f"gz_root_index_file={index_yaml}", + *unknown_args, + ] + subprocess.run(sphinx_args) + + # Create a redirect to "/latest" + redirect_page = build_docs_dir / "index.html" + redirect_page.write_text("""\ + + + + + + +""") + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/citadel/index.yaml b/citadel/index.yaml index f58d5ec5d0..f293b7a473 100644 --- a/citadel/index.yaml +++ b/citadel/index.yaml @@ -41,6 +41,9 @@ pages: - name: troubleshooting title: Troubleshooting file: troubleshooting.md + - name: ign_docker_env + title: Dockerized Dev Env + file: ign_docker_env.md - name: comparison title: Feature Comparison file: comparison.md diff --git a/citadel/install_osx_src.md b/citadel/install_osx_src.md index 77e564cb4a..8c9b78edaa 100644 --- a/citadel/install_osx_src.md +++ b/citadel/install_osx_src.md @@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee Create a file called `intern.patch` with the following content: -```patch +```diff --- intern.h 2019-12-16 18:17:08.000000000 +0100 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h @@ -14,6 +14,10 @@ @@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content: Now we can apply the patch: -```{.sh} +```sh sudo patch -p0 < intern.patch ``` @@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content: Now we can appply the patch: -```{.sh} +```sh sudo patch -p0 < config.patch ``` diff --git a/citadel/ros_integration.md b/citadel/ros_integration.md index 460a698549..22184cefa4 100644 --- a/citadel/ros_integration.md +++ b/citadel/ros_integration.md @@ -96,4 +96,4 @@ The screenshot shows all the shell windows and their expected content (it was taken using ROS Melodic): -![Ignition Transport images and ROS 1 rqt](../acropolis/images/bridge_image_exchange_ign-gazebo.png) +![Ignition Transport images and ROS 1 rqt](img/bridge_image_exchange_ign-gazebo.png) diff --git a/citadel/sensors.md b/citadel/sensors.md index c5f8f101fe..34d0e120a8 100644 --- a/citadel/sensors.md +++ b/citadel/sensors.md @@ -362,14 +362,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/citadel/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory: -```{.sh} +```sh mkdir build cd build ``` Run cmake and build the code: -```{.sh} +```sh cmake .. make lidar_node ``` @@ -378,13 +378,13 @@ make lidar_node Run the node from terminal 1: -```{.sh} +```sh ./build/lidar_node ``` Run the world from terminal 2: -```{.sh} +```sh ign gazebo sensor_tutorial.sdf ``` @@ -413,7 +413,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world. And the second command is `./build/lidar_node` which runs the `lidar_node`. Save the file as `sensor_launch.ign`, and then run it using the following command: -```{.sh} +```sh ign launch sensor_launch.ign ``` diff --git a/conf.py b/conf.py new file mode 100644 index 0000000000..6145489349 --- /dev/null +++ b/conf.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import sys +from pathlib import Path +import yaml + + +from sphinx.application import Sphinx +from sphinx.config import Config + + +sys.path.append(str(Path(__file__).parent)) +from base_conf import * # noqa + +html_baseurl = f"{html_context['deploy_url']}/docs/latest" # noqa + +html_context.update({ + "github_user": "gazebosim", + "github_repo": "docs", + "github_version": "master", + "edit_page_url_template": "{{ github_url }}/{{ github_user }}/{{ github_repo }}" + "/edit/{{ github_version }}/{{ get_file_from_map(file_name) }}", + "edit_page_provider_name": "GitHub", +}) + + +def setup_file_map(app: Sphinx, pagename: str, templatename: str, context, doctree): + def get_file_from_map(file_name: str): + result = context["file_name_map"].get(Path(file_name).stem) + if result: + return result + return file_name + + context["get_file_from_map"] = get_file_from_map + + +def load_releases(index_file): + with open(index_file) as top_index_file: + gz_nav_yaml = yaml.safe_load(top_index_file) + + return dict([(release["name"], release) for release in gz_nav_yaml["releases"]]) + + +def get_preferred_release(releases: dict): + preferred = [rel for rel in releases.values() if rel.get("preferred", False)] + assert len(preferred) == 1 + return preferred[0] + + +def create_file_rename_map(nav_yaml_pages, release): + file_name_map = {} + + prefix = f"{release}/" if release is not None else "" + + for page in nav_yaml_pages: + file_name_map[page["name"]] = f"{prefix}{page['file']}" + + children = page.get("children") + if children: + file_name_map.update(create_file_rename_map(children, release)) + + return file_name_map + + +def config_init(app: Sphinx, config: Config): + if not config.gz_release: + raise RuntimeError("gz_release not provided") + config.release = config.gz_release # type: ignore + config.version = config.gz_release # type: ignore + + file_name_map = {} + + with open(app.config.gz_root_index_file) as f: + file_name_map.update(create_file_rename_map(yaml.safe_load(f)["pages"], None)) + + with open(Path(app.srcdir) / "index.yaml") as f: + file_name_map.update( + create_file_rename_map(yaml.safe_load(f)["pages"], config.release) + ) + + config.html_context["file_name_map"] = file_name_map + + # We've disabled "check_switcher" since it doesn't play well with our directory structure. + # So we check for the existence of switcher.json here + # + assert Path(f"{app.srcdir}/_static/switcher.json").exists() + config.html_theme_options["switcher"] = { + "json_url": f"{html_context['deploy_url']}/docs/{config.gz_release}/_static/switcher.json", + "version_match": config.gz_release, + } + + try: + releases = load_releases(config.gz_root_index_file) + app.config.html_context["release_info"] = releases[config.gz_release] + app.config.html_context["preferred_release"] = get_preferred_release(releases) + + except KeyError as e: + print(e) + raise RuntimeError( + f"Provided gz_release '{config.gz_release}' not registered in `index.yaml`" + ) + + +def setup(app: Sphinx): + app.add_config_value("gz_release", "", rebuild="env", types=[str]) + app.add_config_value("gz_root_index_file", "", rebuild="env", types=[str]) + app.connect("html-page-context", setup_file_map) + app.connect("config-inited", config_init) diff --git a/contributing.md b/contributing.md index b7c3c4d5ec..0c5459a259 100644 --- a/contributing.md +++ b/contributing.md @@ -8,29 +8,6 @@ Organization](https://github.com/gazebosim) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. -#### Table of Contents - -[Code of Conduct](https://gazebosim.org/docs/all/contributing#code-of-conduct) - -[Project Design](https://gazebosim.org/docs/all/contributing#project-design) - - * [Repository List](https://gazebosim.org/docs/all/contributing#repository-list) - -[How to Contribute](https://gazebosim.org/docs/all/contributing#how-to-contribute) - - * [Reporting Bugs](https://gazebosim.org/docs/all/contributing#reporting-bugs) - * [Suggesting Enhancements](https://gazebosim.org/docs/all/contributing#suggesting-enhancements) - * [Contributing Code](https://gazebosim.org/docs/all/contributing#contributing-code) - * [Tracking Progress](https://gazebosim.org/docs/all/contributing#tracking-progress) - -[Writing Tests](https://gazebosim.org/docs/all/contributing#writing-tests) - - * [Test Coverage](https://gazebosim.org/docs/all/contributing#test-coverage) - -[Styleguides](https://gazebosim.org/docs/all/contributing#style-guides) - -[Appendix](https://gazebosim.org/docs/all/contributing#appendix) - ## Code of Conduct This project and everyone participating in it is governed by the [Gazebo diff --git a/dome/install_osx_src.md b/dome/install_osx_src.md index ceaa16d487..2c4e5ceae0 100644 --- a/dome/install_osx_src.md +++ b/dome/install_osx_src.md @@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee Create a file called `intern.patch` with the following content: -```patch +```diff --- intern.h 2019-12-16 18:17:08.000000000 +0100 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h @@ -14,6 +14,10 @@ @@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content: Now we can apply the patch: -```{.sh} +```sh sudo patch -p0 < intern.patch ``` @@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content: Now we can appply the patch: -```{.sh} +```sh sudo patch -p0 < config.patch ``` diff --git a/dome/sensors.md b/dome/sensors.md index 31ab5597ec..6bb641cfd8 100644 --- a/dome/sensors.md +++ b/dome/sensors.md @@ -362,14 +362,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/dome/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory: -```{.sh} +```sh mkdir build cd build ``` Run cmake and build the code: -```{.sh} +```sh cmake .. make lidar_node ``` @@ -378,13 +378,13 @@ make lidar_node Run the node from terminal 1: -```{.sh} +```sh ./build/lidar_node ``` Run the world from terminal 2: -```{.sh} +```sh ign gazebo sensor_tutorial.sdf ``` @@ -413,7 +413,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world. And the second command is `./build/lidar_node` which runs the `lidar_node`. Save the file as `sensor_launch.ign`, and then run it using the following command: -```{.sh} +```sh ign launch sensor_launch.ign ``` diff --git a/dome/web_visualization.md b/dome/web_visualization.md index 97239a74ee..4cd1a1c01f 100644 --- a/dome/web_visualization.md +++ b/dome/web_visualization.md @@ -62,7 +62,7 @@ matching key using an "auth" call on the websocket. If the ` **NOTE** > As Gazebo GUI is not yet working, running `gz sim` will not work. You can run only the server with -> ```cmd +> ```bat > gz sim -s -v > ``` @@ -152,7 +152,7 @@ page to start using Gazebo! > If your username contains spaces (which is quite common on Windows), you will probably get errors > saying `Invalid partition name [Computer:My User With Spaces]`. Fix this by changing `GZ_PARTITION` > to something else: -> ```cmd +> ```bat > set GZ_PARTITION=test > ``` > Remember to set the same partition in all other consoles. diff --git a/harmonic/migration_from_ignition.md b/harmonic/migration_from_ignition.md index 30e5da93ae..d630b85584 100644 --- a/harmonic/migration_from_ignition.md +++ b/harmonic/migration_from_ignition.md @@ -239,7 +239,7 @@ In `CMakeLists.txt` files (and their references in your source files!): **Variables and macro/function calls** -```cpp +``` Find: IGN(ITION)?_GAZEBO Replace: GZ_SIM @@ -255,7 +255,7 @@ Replace: gz_ **Includes** -```cpp +``` Find: include\(Ign Replace: include(Gz @@ -271,7 +271,7 @@ Replace: gz_find_package(Gz- **Project Names** -```cpp +``` Find: ignition-gazebo Replace: gz-sim @@ -288,7 +288,7 @@ Replace: gz- Migrate source macros and environment variables -```cpp +``` Find: IGN(ITION)?_GAZEBO Replace: GZ_SIM @@ -319,7 +319,7 @@ Additionally, the logging macros have also been migrated! Migrate any uses! In `.sdf` files: -```cpp +``` Find: **NOTE** > As Gazebo GUI is not yet working, running `gz sim` will not work. You can run only the server with -> ```cmd +> ```batch > gz sim -s -v > ``` @@ -152,7 +152,7 @@ page to start using Gazebo! > If your username contains spaces (which is quite common on Windows), you will probably get errors > saying `Invalid partition name [Computer:My User With Spaces]`. Fix this by changing `GZ_PARTITION` > to something else: -> ```cmd +> ```batch > set GZ_PARTITION=test > ``` > Remember to set the same partition in all other consoles. diff --git a/ionic/migration_from_ignition.md b/ionic/migration_from_ignition.md index 6798db1d87..5d0eafa300 100644 --- a/ionic/migration_from_ignition.md +++ b/ionic/migration_from_ignition.md @@ -239,7 +239,7 @@ In `CMakeLists.txt` files (and their references in your source files!): **Variables and macro/function calls** -```cpp +``` Find: IGN(ITION)?_GAZEBO Replace: GZ_SIM @@ -255,7 +255,7 @@ Replace: gz_ **Includes** -```cpp +``` Find: include\(Ign Replace: include(Gz @@ -271,7 +271,7 @@ Replace: gz_find_package(Gz- **Project Names** -```cpp +``` Find: ignition-gazebo Replace: gz-sim @@ -288,7 +288,7 @@ Replace: gz- Migrate source macros and environment variables -```cpp +``` Find: IGN(ITION)?_GAZEBO Replace: GZ_SIM @@ -319,7 +319,7 @@ Additionally, the logging macros have also been migrated! Migrate any uses! In `.sdf` files: -```cpp +``` Find: WARNING: this document is no more than a list of steps. Check with the infra-team diff --git a/releasing/release_repositories.md b/releasing/release_repositories.md index bddbdf3d1b..52b7157bca 100644 --- a/releasing/release_repositories.md +++ b/releasing/release_repositories.md @@ -1,3 +1,6 @@ +--- +orphan: true +--- # Release repositories > TODO: the document needs to be completed with real information. The points diff --git a/releasing/versioning_pre_nightly.md b/releasing/versioning_pre_nightly.md index 80e7b36fcc..ed90409899 100644 --- a/releasing/versioning_pre_nightly.md +++ b/releasing/versioning_pre_nightly.md @@ -1,4 +1,4 @@ -## Debian/Ubuntu versioning in nightly and prerelease binaries +# Debian/Ubuntu versioning in nightly and prerelease binaries Binary packages produced for prerelease and nightly builds have some particularities to establish the priority among them nicely. @@ -17,7 +17,7 @@ this precedence, the nighlty version uses the trick of setting the version to `{X-1.99.99}` (i.e: if the version to release is `9.0.0`, the nightlies used before the version will use `8.99.99`). -### Version schemes +## Version schemes **Prerelease** versioning scheme: `{upcoming_version}~pre{prerelease_version}` @@ -39,7 +39,7 @@ the version will use `8.99.99`). * `nightly_revision`: revision number to apply to the nightly. It is also used to generate a new nightly using the same date timestamp. -### Versions when mixing stable, prerelease and nightly +## Versions when mixing stable, prerelease and nightly Which version has priority when using prerelease and stable repositories? diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..78dc33dcab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +myst-parser[linkify] +pydata-sphinx-theme +pyyaml +sphinx +sphinx-copybutton +sphinx-design +requests diff --git a/ros_installation.md b/ros_installation.md index 40eb95fb2d..5518ad1780 100644 --- a/ros_installation.md +++ b/ros_installation.md @@ -98,9 +98,9 @@ versions that have the same major number (`gz-sim7_7.0.0`, `gz-sim7_7.1.0`, `gz-sim7_7.0.1`, ...) are binary compatible and thus interchangeable with a given ROS distro. -## Installing Gazebo +### Installing Gazebo -### Gazebo Packages for Ubuntu +#### Gazebo Packages for Ubuntu The easiest way of installing Gazebo on Ubuntu is to use binary packages. There are two main repositories that host Gazebo simulator and Gazebo libraries: one @@ -254,7 +254,7 @@ Getting the latest versions of the Gazebo libraries and simulator is as easy as installing the [`osrfoundation.org` repository](https://gazebosim.org/docs/latest/install_ubuntu_src#install-dependencies) together with the ROS repository. Updates should be fully compatible. -## FAQ +### FAQ #### I am not using ROS at all, which version should I use? diff --git a/tutorials.yaml b/tutorials.yaml deleted file mode 100644 index 72ec21e964..0000000000 --- a/tutorials.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# This file is an index of the pages to display on the documentation website -# (https://gazebosim.org/tutorials). The order of the pages in this file -# is reflected on the website's left sidebar. - -releases: - - citadel - - blueprint - - acropolis