diff --git a/.gitignore b/.gitignore index 360f7e848f..841979c4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ _processed/tutes/ _processed/rust/ _data/microkit_tutorial.yml _data/microkit_platforms.yml +projects/microkit/manual/dev/ diff --git a/projects/microkit/manual/2.2.0/index.md b/projects/microkit/manual/2.2.0/index.md index 02867605f8..5acc5c704d 100644 --- a/projects/microkit/manual/2.2.0/index.md +++ b/projects/microkit/manual/2.2.0/index.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0 mathjax: true --- -# Microkit User Manual (2.2.0) +# Microkit User Manual (v2.2.0) This is a web version of the manual, you can find a PDF [here](microkit_user_manual.pdf) as well as in the SDK at `doc/microkit_user_manual.pdf`. diff --git a/tools/import_microkit_manual.py b/tools/import_microkit_manual.py new file mode 100755 index 0000000000..5a344a0a1a --- /dev/null +++ b/tools/import_microkit_manual.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 + +# Copyright 2026 seL4 International +# SPDX-License-Identifier: BSD-2-Clause + +"""Import the Microkit manual from _repos/sel4/microkit + +Usage: + import_microkit_manual.py [] + +Read the manual from the current checkout of the microkit repo in +_repos/sel4/microkit, and transform it into docsite format. + +If a directory is provided, write the output to projects/microkit/manual//, +and point projects/microkit/manual/latest/ at it. + +If no directory is provided, write the output to projects/microkit/manual/dev/. + +Writes the following files: +- projects/microkit/manual//index.md +- projects/microkit/manual//assets/microkit_flow.svg +- projects/microkit/manual//microkit_user_manual.pdf +""" + +# This script is very ad hoc and makes many hardcoded assumptions about the +# structure of manual.md. Will need updating if that structure changes. + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +DOCS_ROOT = Path(__file__).resolve().parent.parent +MICROKIT_REPO = DOCS_ROOT / '_repos' / 'sel4' / 'microkit' +MANUAL_DST_BASE = DOCS_ROOT / 'projects' / 'microkit' / 'manual' + + +def skip_blank(lines: list[str], i: int) -> int: + """Return the index of the first non-blank line at or after i.""" + return next((j for j in range(i, len(lines)) if lines[j].strip()), len(lines)) + + +def transform_manual(source: str, target: str, git_hash: str) -> str: + """Transform microkit manual.md into a docsite-compatible index.md.""" + + copyright_text = '2021, Breakaway Consulting Pty. Ltd.' + + lines = source.splitlines() + i = 0 + + # Skip initial HTML comment block + if lines and lines[0].strip() == '') + 1 + i = skip_blank(lines, i) + + if not lines[i].strip() == '---': + sys.exit('error: expected LaTeX YAML front matter after initial comment block') + + # Skip the LaTeX YAML front matter + i += 1 + while lines[i].strip() != '---': + i += 1 + i = skip_blank(lines, i + 1) + + # Skip LaTeX lines (beginning with '\') + while lines[i].startswith('\\'): + i += 1 + i = skip_blank(lines, i) + + remaining = lines[i:] + + # Remove all \clearpage lines + blank line after + cleaned: list[str] = [] + j = 0 + while j < len(remaining): + if remaining[j].strip() == r'\clearpage': + j += 1 + # drop one following blank line if present + if j < len(remaining) and not remaining[j].strip(): + j += 1 + else: + cleaned.append(remaining[j]) + j += 1 + + # increase every heading level by one + headed = ['#' + line if line.startswith('#') else line for line in cleaned] + + # replace pdf image path by svg + body = '\n'.join(headed) + body = body.replace('microkit_flow.pdf', 'microkit_flow.svg') + body = body.strip() + + # docsite front matter + version = f'dev-{git_hash}' if target == 'dev' else f'v{target}' + new_header = ( + '---\n' + 'parent: /projects/microkit/manual/latest/\n' + 'toc: true\n' + f'SPDX-FileCopyrightText: {copyright_text}\n' + # add random space in here so spdx checker does not barf + 'SPDX-' 'License-Identifier: CC-BY-SA-4.0\n' + 'mathjax: true\n' + '---\n' + '\n' + f'# Microkit User Manual ({version})\n' + '\n' + 'This is a web version of the manual, you can find a PDF ' + '[here](microkit_user_manual.pdf) as well as in the\n' + 'SDK at `doc/microkit_user_manual.pdf`.\n' + '\n' + ) + return new_header + body + '\n' + + +def update_latest_symlink(tag: str) -> None: + """Point projects/microkit/manual/latest at the new tag directory.""" + latest = MANUAL_DST_BASE / 'latest' + if latest.is_symlink(): + latest.unlink() + else: + sys.exit('error: expected `latest` to be a symlink') + os.symlink(tag, latest) + print(f'updated latest -> {tag}') + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('dir', nargs='?', default='dev', + help='Directory to write the manual to (default: dev)') + args = parser.parse_args() + + if not MICROKIT_REPO.is_dir(): + sys.exit(f'error: microkit repo not found at {MICROKIT_REPO}\n' + f'Run "make repos" first.') + + target_dir = args.dir + git_hash = subprocess.check_output( + ['git', 'rev-parse', '--short=6', 'HEAD'], + cwd=MICROKIT_REPO, text=True + ).strip() + + dst = MANUAL_DST_BASE / target_dir + assets_dst = dst / 'assets' + + # Create destination directories + dst.mkdir(parents=True, exist_ok=True) + assets_dst.mkdir(exist_ok=True) + + # Transform and write index.md + input_path = MICROKIT_REPO / 'docs' / 'manual.md' + output_path = dst / 'index.md' + manual_src = input_path.read_text() + index_md = transform_manual(manual_src, target_dir, git_hash) + output_path.write_text(index_md) + print(f'wrote {output_path.relative_to(DOCS_ROOT)}') + + # Copy the SVG flow diagram + input_path = MICROKIT_REPO / 'docs' / 'assets' / 'microkit_flow.svg' + output_path = assets_dst / 'microkit_flow.svg' + shutil.copy(input_path, output_path) + print(f'wrote {output_path.relative_to(DOCS_ROOT)}') + + # Build PDF in the microkit repo and copy to the target directory + print(f'Building PDF from {(MICROKIT_REPO / 'docs' / 'manual.md').relative_to(DOCS_ROOT)}') + input_path = MICROKIT_REPO / 'docs' / 'manual.pdf' + output_path = dst / 'microkit_user_manual.pdf' + subprocess.run( + ['pandoc', 'manual.md', '-o', 'manual.pdf'], + cwd=MICROKIT_REPO / 'docs', + env={**os.environ, 'TEXINPUTS': 'style:'}, + check=True + ) + shutil.copy(input_path, output_path) + print(f'wrote {output_path.relative_to(DOCS_ROOT)}') + + # Update symlink if not dev + if target_dir != 'dev': + update_latest_symlink(target_dir) + + +if __name__ == '__main__': + main()