diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index 01502b13..f1f79481 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -1,6 +1,6 @@ # This is a basic workflow to help you get started with Actions -name: CI +name: Build and deploy all docs to github pages. # Controls when the workflow will run on: @@ -10,12 +10,17 @@ on: pull_request: branches: [ "main" ] +permissions: + contents: read + pages: write + id-token: write + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" + build: # The type of runner that the job will run on runs-on: ubuntu-latest @@ -27,10 +32,19 @@ jobs: # Runs a single command using the runners shell - name: Run a one-line script - run: echo Hello, world! + run: | + ./tools/build.sh + + - uses: actions/upload-pages-artifact@v3 + with: + path: site - # Runs a set of commands using the runners shell - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b023e8ab..40044042 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ generated/xcore/contab/netex.html templates/README.md~ tools/schematron_builder/__pycache__/template2schematron.cpython-313.pyc __pycache__ -netex_rg_ch.egg-info \ No newline at end of file +netex_rg_ch.egg-info +dist/ +site/ +build/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 82bfc82b..d4873bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools>=70", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "netex-rg-ch" -version = "0.1.0" +version = "0.1.1" description = "Provides tools to help building the NeTEx RG." requires-python = ">=3.13" dependencies = [ @@ -23,4 +23,10 @@ xml-snippets="tools.xml_snippets.build_xml_snippets:main" # Only package the specified packages [tool.setuptools.packages.find] -include = ["tools*"] \ No newline at end of file +include = ["tools*"] + +[tool.setuptools.package-data] +"expanded_docs" = ["generated/docs/**/*.md"] + +[options.entry_points."distutils.commands"] +expand_docs = "tools.expand_docs.ExpandDocs" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..c2be3921 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +# setup.py - tiny hook to build docs into ./site) +import os +import subprocess +import sys +from pathlib import Path +from setuptools import setup, Command +from setuptools.command.sdist import sdist as _sdist + +class build_docs(Command): + """Generate documentation into ./site.""" + description = "Build project documentation into ./site" + user_options = [ + ("clean", None, "clean the output directory before building"), + ] + + def initialize_options(self): + self.clean = False + + def finalize_options(self): + self.clean = bool(self.clean) + + def run(self): + outdir = Path("site") + if self.clean and outdir.exists(): + import shutil + shutil.rmtree(outdir) + outdir.mkdir(exist_ok=True) + + # run tools here ... + cmd = [sys.executable, "-m", "tools.expand_docs.expand_docs", "--out", str(outdir)] + self.announce(f"Running: {' '.join(cmd)}", level=2) + # Build environment variables if you want to pass context + env = dict(os.environ) + env.setdefault("PYTHONHASHSEED", "0") + subprocess.check_call(cmd, env=env) # fails the build on nonzero exit + self.announce(f"Docs generated in {outdir}", level=2) + +class sdist(_sdist): + def run(self): + # Build docs before sdist (writes to ./site but we will exclude it from sdist) + self.run_command("build_docs") + super().run() + +cmdclass = {"build_docs": build_docs, "sdist": sdist} + +setup(cmdclass=cmdclass) diff --git a/tools/README.md b/tools/README.md index f60e4e79..0b42c5fc 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,16 +1,53 @@ # Tools for the Swiss NeTEx RG -## Install Tools with uv +## How to setup and run the build -The package manager `uv` simplifies the build and installation of scripts for the tools. +The build builds the tools and runs them to create the generated documents in the directory `site`. -- Dependencies are managed by `uv`, as configured in `pyproject.toml` and more detailed in `uv.lock`. -- `uv` provides an os-independent interface for scripts -- The generated tool scripts run on Windows, Mac or Linux +### Steps involved to setup and run the build + +1. Install the [uv package manager](#install-the-uv-package-manager) +2. Initialize the [virtual environment](#initialize-the-virtual-environment) +3. [Run the build]() + +For more information about the build framework, see [Build Automation](#build-automation). + +### Install the uv package manager + +Install the uv package manager: +- See [uv package manager](https://docs.astral.sh/uv/) +- if you have pip installed, you can run `pip install uv` + +### Initialize the virtual environment + +#### Mac/Linux + +Run the following following commands in the project root directory: +```sh +uv venv +uv sync +sh ./venv/bin/activate +``` + +#### Windows + +Run the following following commands in the project root directory: + +``` shell +uv venv +uv sync +venv\bin\activate.bat +``` -### Install the package manager +### Run the build -See [uv package manager](https://docs.astral.sh/uv/) +If everything is setup correctly, you should be able to run the build doing `python -m build` in the project root directory. + + +## Tool Scripts + +The `pyproject.toml` is configured to generate scripts for the tools. +These tool scripts are not required for the build, but they may be useful for running tools locally. ### Prerequisites: Set PYTHONPATH and PATH @@ -52,4 +89,30 @@ This generates executable scripts for Linux/Mac and Windows in subdirectories of ### How to add a new Script - Add a new entry in the `[project.scripts]` section of `pyproject.toml`. -- If the script requires another package, use `uv add` to added to the environment. \ No newline at end of file +- If the script requires another package, use `uv add` to added to the environment. + +## Build Automation Framework + +### Package Manager + +The package manager `uv` simplifies the build and installation of scripts for the tools. + +- Dependencies are managed by `uv`, as configured in `pyproject.toml` and more detailed in `uv.lock`. +- `uv` provides an os-independent interface for scripts + - Generated tool scripts run on Windows, Mac or Linux + +### Project build + +Components of the build automation: +- [pyproject.toml](../pyproject.toml) is configured with `setuptools` (https://setuptools.pypa.io/en/latest/) + - docs can be generated running `python -m build` +- `setup.py` in the root project acts as the interface for the build + - here we can add tools to be run during the build. +- The build writes all output to directory `site`, excluded from git + +### Github Action + +The Github Action [pages.yaml](../.github/pages.yaml) runs the script [build.sh](./build.sh) (can also be tested locally) + - triggered after commits to main branch (e.g. after the merge of a branch) + - runs the build via the `python -m build` mechanism + - uploads generated docs to GitHub Pages diff --git a/tools/build.sh b/tools/build.sh new file mode 100755 index 00000000..b8a27417 --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Build script for the github action. +# - can also be run and tested locally (Mac or Linux) +# - use "python -m build to build docs otherwise +set -Eeuo pipefail +IFS=$'\n\t' + +# --- Configuration (override via environment variables) --- +PYTHON="${PYTHON_BIN:-python3}" # or "python" if that's your default +VENV_DIR="${VENV_DIR:-.venv-build}" # ephemeral venv only for build tools +OUTDIR="${OUTDIR:-dist}" # where artifacts go +EXTRA_BUILD_ARGS="${EXTRA_BUILD_ARGS:-}" # e.g. "--no-isolation" (not recommended) +TEST_INSTALL="${TEST_INSTALL:-1}" # 1 to smoke-test installing the wheel + +# --- Move to repo root --- +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." &>/dev/null && pwd)" +cd "$REPO_ROOT" + +echo "==> Building package in $REPO_ROOT" + +# --- Pre-flight checks --- +if [[ ! -f pyproject.toml ]]; then + echo "Error: pyproject.toml not found in repository root: $REPO_ROOT" >&2 + exit 1 +fi + +# --- Clean previous build outputs --- +# rm -rf "$OUTDIR" build .pytest_cache +mkdir -p "$OUTDIR" + +# --- Create isolated venv for build tools --- +if [[ ! -d "$VENV_DIR" ]]; then + echo "==> Creating build venv at $VENV_DIR" + $PYTHON -m venv "$VENV_DIR" +fi + +# shellcheck source=/dev/null +source "$VENV_DIR/bin/activate" +$PYTHON -m pip install --upgrade pip setuptools +$PYTHON -m pip install --upgrade build + +# If you use setuptools-scm, ensure it's present for version derivation +if grep -qiE 'setuptools[-_]scm' pyproject.toml; then + $PYTHON -m pip install --upgrade setuptools-scm +fi + +# --- Make builds more reproducible (optional but harmless) --- +export PYTHONHASHSEED=0 +export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct 2>/dev/null || date +%s)}" + +# --- Build sdist and wheel --- +echo "==> Running python -m build" +$PYTHON -m build --sdist --wheel --outdir "$OUTDIR" $EXTRA_BUILD_ARGS + +# --- List outputs --- +echo "==> Produced artifacts:" +ls -lh "$OUTDIR" + +deactivate +echo "==> Build complete." diff --git a/tools/configuration.py b/tools/configuration.py index d780577e..57cc4d12 100644 --- a/tools/configuration.py +++ b/tools/configuration.py @@ -7,6 +7,7 @@ TEMPLATES_DIR = PROJECT_DIR.joinpath("../templates") # Generated documents -GENERATED_DIR = PROJECT_DIR.joinpath("../generated") +GENERATED_DIR = PROJECT_DIR.joinpath("../site") +GENERATED_DOCS_DIR = GENERATED_DIR.joinpath("/docs") XSD_FILE_PATH = PROJECT_DIR.joinpath("../xsd/xsd/NeTEx_publication.xsd") diff --git a/tools/expand_docs/expand_docs.py b/tools/expand_docs/expand_docs.py index 34a3e419..6ba2c3ee 100644 --- a/tools/expand_docs/expand_docs.py +++ b/tools/expand_docs/expand_docs.py @@ -6,6 +6,12 @@ import shutil import argparse import re +from abc import ABC + +from setuptools import Command + +from tools.configuration import DOCS_DIR, GENERATED_DOCS_DIR + def copy_media_folder(input_folder, output_folder): """Copy media folder from input to output.""" @@ -73,8 +79,8 @@ def process_markdown_file(input_path, output_path, base_folder): def main(): parser = argparse.ArgumentParser(description='Expand documentation by including examples and tables.') - parser.add_argument('--docs', required=True, help='Input documentation folder') - parser.add_argument('--out', required=True, help='Output folder') + parser.add_argument('--docs', default=DOCS_DIR, help=f"Input documentation folder (default = {DOCS_DIR})") + parser.add_argument('--out', default=GENERATED_DOCS_DIR, help=f"Output folder (default = {GENERATED_DOCS_DIR})") args = parser.parse_args() # Create output folder if it doesn't exist @@ -93,4 +99,16 @@ def main(): print(f"Documentation expanded successfully to {args.out}") if __name__ == '__main__': - main() \ No newline at end of file + main() + +class ExpandDocs(Command, ABC): + """Setuptools plugin for the project build.""" + + def run(self) -> None: + """ + Execute the actions intended by the command. + (Side effects **SHOULD** only take place when :meth:`run` is executed, + for example, creating new files or writing to the terminal output). + """ + main() + pass \ No newline at end of file