Fix bilingual tutorial indexes and internal links (#209)

* Fix bilingual tutorial index generation

* Check relative Markdown links in CI

* Harden documentation regression checks

* Reduce link parser complexity

* Run link checks for every repository change

---------

Co-authored-by: LinuxDev9002 <i@littlefisher.me>
This commit is contained in:
云微
2026-07-14 04:10:51 -07:00
committed by GitHub
parent 8bfc912d6a
commit e08f604769
11 changed files with 456 additions and 37 deletions

View File

@@ -19,7 +19,7 @@ The tutorial focuses on eBPF examples in observability, networking, security, an
## Why write this tutorial?
In the process of learning eBPF, we have been inspired and helped by the [bcc python developer tutorial](src/bcc-documents/tutorial_bcc_python_developer.md). However, from the current perspective, using `libbpf` to develop eBPF applications is a relatively better choice.
In the process of learning eBPF, we have been inspired and helped by the [bcc Python developer tutorial](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md). However, from the current perspective, using `libbpf` to develop eBPF applications is a relatively better choice.
This project is mainly based on [libbpf](https://github.com/libbpf/libbpf) frameworks.

View File

@@ -4,7 +4,7 @@
[GitHub](https://github.com/eunomia-bpf/bpf-developer-tutorial)
[Gitee 镜像](https://gitee.com/yunwei37/bpf-developer-tutorial)
[English Version](README_en.md)
[English Version](README.md)
#### [**Check out the English version here**](README.md)
@@ -18,7 +18,7 @@
## 为什么要写这个教程?
在学习 eBPF 的过程中,我们受到了 [bcc python developer tutorial](src/bcc-documents/tutorial_bcc_python_developer.md) 的许多启发和帮助,但从当下的角度出发,使用 libbpf 开发 eBPF 的应用是目前相对更好的选择。但目前似乎很少有基于 libbpf 和 BPF CO-RE 出发的、通过案例和工具介绍 eBPF 开发的教程,因此我们发起了这个项目,采用类似 bcc python developer tutorial 的组织方式,但使用 CO-RE 的 libbpf 进行开发。
在学习 eBPF 的过程中,我们受到了 [bcc Python developer tutorial](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md) 的许多启发和帮助,但从当下的角度出发,使用 libbpf 开发 eBPF 的应用是目前相对更好的选择。但目前似乎很少有基于 libbpf 和 BPF CO-RE 出发的、通过案例和工具介绍 eBPF 开发的教程,因此我们发起了这个项目,采用类似 bcc Python developer tutorial 的组织方式,但使用 CO-RE 的 libbpf 进行开发。
本项目主要基于 [libbpf-boostrap](https://github.com/libbpf/libbpf-bootstrap) 和 [eunomia-bpf](https://github.com/eunomia-bpf/eunomia-bpf) 两个框架完成,并使用 eunomia-bpf 帮助简化一部分 libbpf eBPF 用户态代码的编写,让开发者专注于内核态的 eBPF 代码的开发。
@@ -146,8 +146,6 @@ eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的
![ebpf-chatgpt-signal](imgs/ebpf-chatgpt-signal.png)
完整的对话记录可以在这里找到: [ChatGPT.md](src/ChatGPT.md)
我们也构建了一个命令行工具的 demo ,通过本教程的训练, 让它通过自然语言描述即可自动编写 eBPF 程序,追踪 Linux 系统:<https://github.com/eunomia-bpf/GPTtrace>
![ebpf-chatgpt-signal](https://github.com/eunomia-bpf/GPTtrace/blob/main/doc/result.gif)

View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Check relative Markdown links and images without making network requests."""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import unquote, urlsplit
INLINE_LINK_PATTERN = re.compile(r"(?<!\\)\]\(")
REFERENCE_DEFINITION_PATTERN = re.compile(r"^ {0,3}\[[^]]+\]:[ \t]*(.*)$")
FENCE_PATTERN = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
MARKDOWN_ESCAPE_PATTERN = re.compile(r"\\([!\"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~])")
IGNORED_SCHEMES = {"data", "http", "https", "javascript", "mailto"}
@dataclass(frozen=True)
class BrokenLink:
source: Path
line: int
destination: str
def markdown_files(root: Path) -> list[Path]:
return sorted(
path
for path in root.rglob("*.md")
if not {".git", "build", "node_modules", "site"}.intersection(path.parts)
)
def strip_code_spans(line: str) -> str:
"""Blank inline code while preserving character offsets."""
result = list(line)
position = 0
while position < len(line):
if line[position] != "`":
position += 1
continue
delimiter_end = position
while delimiter_end < len(line) and line[delimiter_end] == "`":
delimiter_end += 1
delimiter_length = delimiter_end - position
closing = delimiter_end
while closing < len(line):
if line[closing] != "`":
closing += 1
continue
closing_end = closing
while closing_end < len(line) and line[closing_end] == "`":
closing_end += 1
if closing_end - closing == delimiter_length:
break
closing = closing_end
if closing >= len(line):
position = delimiter_end
continue
result[position : closing + delimiter_length] = " " * (
closing + delimiter_length - position
)
position = closing + delimiter_length
return "".join(result)
def parse_angle_destination(text: str, position: int) -> str:
destination: list[str] = []
while position < len(text):
if text[position] == "\\" and position + 1 < len(text):
destination.extend(text[position : position + 2])
position += 2
elif text[position] == ">":
return "".join(destination)
else:
destination.append(text[position])
position += 1
return ""
def parse_bare_destination(text: str, position: int) -> str:
destination = []
parentheses = 0
while position < len(text):
character = text[position]
if character == "\\" and position + 1 < len(text):
destination.extend(text[position : position + 2])
position += 2
continue
if character == "(":
parentheses += 1
elif character == ")":
if parentheses == 0:
break
parentheses -= 1
elif character.isspace() and parentheses == 0:
break
destination.append(character)
position += 1
return "".join(destination) if parentheses == 0 else ""
def parse_destination(text: str, start: int = 0) -> str:
"""Parse one CommonMark-style link destination from text."""
position = start
while position < len(text) and text[position] in " \t":
position += 1
if position >= len(text):
return ""
if text[position] == "<":
return parse_angle_destination(text, position + 1)
return parse_bare_destination(text, position)
def destinations_in_line(line: str) -> list[str]:
without_code = strip_code_spans(line)
destinations = [
parse_destination(without_code, match.end())
for match in INLINE_LINK_PATTERN.finditer(without_code)
]
reference = REFERENCE_DEFINITION_PATTERN.match(without_code)
if reference:
destinations.append(parse_destination(reference.group(1)))
return destinations
def is_ignored(destination: str) -> bool:
parsed = urlsplit(destination)
return (
not destination
or destination.startswith(("#", "/"))
or parsed.scheme.lower() in IGNORED_SCHEMES
or any(marker in destination for marker in ("${", "{{", "}}"))
)
def filesystem_path(destination: str) -> str:
decoded = unquote(urlsplit(destination).path)
return MARKDOWN_ESCAPE_PATTERN.sub(r"\1", decoded)
def find_broken_links(root: Path) -> list[BrokenLink]:
root = root.resolve()
broken: list[BrokenLink] = []
for source in markdown_files(root):
fence: tuple[str, int] | None = None
for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), 1):
match = FENCE_PATTERN.match(line)
if fence is not None:
marker, minimum_length = fence
if re.fullmatch(
rf" {{0,3}}{re.escape(marker)}{{{minimum_length},}}[ \t]*", line
):
fence = None
continue
if match:
marker = match.group(1)
if not (marker[0] == "`" and "`" in match.group(2)):
fence = (marker[0], len(marker))
continue
for destination in destinations_in_line(line):
if is_ignored(destination):
continue
relative_path = filesystem_path(destination)
if relative_path and not (source.parent / relative_path).exists():
broken.append(
BrokenLink(source.relative_to(root), line_number, destination)
)
return broken
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"root",
nargs="?",
type=Path,
default=Path(__file__).resolve().parent.parent,
help="repository root (defaults to the parent of scripts/)",
)
args = parser.parse_args()
broken = find_broken_links(args.root)
if not broken:
print("All relative Markdown links resolve.")
return 0
print(f"Found {len(broken)} broken relative Markdown link(s):", file=sys.stderr)
for item in broken:
print(
f"{item.source}:{item.line}: {item.destination}",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -25,7 +25,8 @@ def generate_toc(base_dir, project_root, output_file_dir):
# To ensure numeric sorting of directories
def sort_key(directory_name):
return list(map(int, re.findall(r'\d+', directory_name)))
numbers = tuple(map(int, re.findall(r'\d+', directory_name)))
return (0, numbers, directory_name) if numbers else (1, (), directory_name)
sections = {} # {section_level: {subsection_type: [lessons]}}
@@ -48,7 +49,7 @@ def generate_toc(base_dir, project_root, output_file_dir):
all_dirs.append(os.path.join(item, subitem))
# Sort directories properly by numeric order (non-numeric dirs go to end)
all_dirs = sorted(all_dirs, key=lambda d: sort_key(d) if re.search(r'\d+', d) else [999999])
all_dirs = sorted(all_dirs, key=sort_key)
# Loop over the sorted directories
for directory in all_dirs:
@@ -142,21 +143,22 @@ def generate_toc_cn(base_dir, project_root, output_file_dir):
}
subsection_titles = {
"Android": "Android:\n\n",
"GPU": "GPU:\n\n",
"Scheduler": "调度器:\n\n",
"Networking": "网络:\n\n",
"tracing": "Tracing:\n\n",
"Security": "安全:\n\n",
"Features": "特性:\n\n",
"Other": "特性:\n\n"
"Android": "\n\nAndroid:\n\n",
"GPU": "\n\nGPU:\n\n",
"Scheduler": "\n\n调度器:\n\n",
"Networking": "\n\n网络:\n\n",
"Tracing": "\n\n追踪:\n\n",
"Security": "\n\n安全:\n\n",
"Features": "\n\n特性:\n\n",
"Other": "\n\n其他:\n\n"
}
subsection_order = ['GPU', 'Scheduler', 'Networking', 'tracing', 'Security', 'Features', 'Other', 'Android']
subsection_order = ['GPU', 'Scheduler', 'Networking', 'Tracing', 'Security', 'Features', 'Other', 'Android']
# To ensure numeric sorting of directories
def sort_key(directory_name):
return list(map(int, re.findall(r'\d+', directory_name)))
numbers = tuple(map(int, re.findall(r'\d+', directory_name)))
return (0, numbers, directory_name) if numbers else (1, (), directory_name)
sections = {} # {section_level: {subsection_type: [lessons]}}
@@ -179,7 +181,7 @@ def generate_toc_cn(base_dir, project_root, output_file_dir):
all_dirs.append(os.path.join(item, subitem))
# Sort directories properly by numeric order (non-numeric dirs go to end)
all_dirs = sorted(all_dirs, key=lambda d: sort_key(d) if re.search(r'\d+', d) else [999999])
all_dirs = sorted(all_dirs, key=sort_key)
# Loop over the sorted directories
for directory in all_dirs:

View File

@@ -0,0 +1,94 @@
import tempfile
import unittest
from pathlib import Path
from scripts.check_internal_links import find_broken_links
class InternalLinkCheckerTest(unittest.TestCase):
def test_accepts_existing_paths_and_ignores_non_file_links(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "guide").mkdir()
(root / "guide" / "target file.md").write_text("# Target\n", encoding="utf-8")
(root / "guide_(v1).md").write_text("# Target\n", encoding="utf-8")
(root / "README.md").write_text(
"\n".join(
[
"[guide](guide/)",
"[encoded](guide/target%20file.md)",
"[with title](guide/target%20file.md \"Target\")",
"[parentheses](guide_(v1).md)",
"[reference][target]",
"[target]: guide/target%20file.md \"Target\"",
"[anchor](#section)",
"[site route](/tutorials/)",
"[external](https://example.com/missing)",
"```markdown",
"[example](missing-example.md)",
"```",
"`[inline example](missing-inline.md)`",
"````markdown",
"```",
"[nested example](missing-nested.md)",
"```",
"````",
]
),
encoding="utf-8",
)
self.assertEqual(find_broken_links(root), [])
def test_reports_missing_relative_link_with_source_line(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "README.md").write_text(
"# Guide\n\n[missing](docs/missing.md)\n", encoding="utf-8"
)
broken = find_broken_links(root)
self.assertEqual(len(broken), 1)
self.assertEqual(broken[0].source, Path("README.md"))
self.assertEqual(broken[0].line, 3)
self.assertEqual(broken[0].destination, "docs/missing.md")
def test_reports_missing_reference_destination(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "README.md").write_text(
"[guide][docs]\n[docs]: missing_(v1).md\n", encoding="utf-8"
)
broken = find_broken_links(root)
self.assertEqual(len(broken), 1)
self.assertEqual(broken[0].line, 2)
self.assertEqual(broken[0].destination, "missing_(v1).md")
def test_closes_fence_only_with_a_long_enough_matching_delimiter(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "README.md").write_text(
"\n".join(
[
"````markdown",
"```",
"[example](ignored.md)",
"````",
"[missing](reported.md)",
]
),
encoding="utf-8",
)
broken = find_broken_links(root)
self.assertEqual(len(broken), 1)
self.assertEqual(broken[0].line, 5)
self.assertEqual(broken[0].destination, "reported.md")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,49 @@
import re
import unittest
from pathlib import Path
from scripts.generate_toc import generate_toc, generate_toc_cn
ROOT = Path(__file__).resolve().parent.parent
LINK_PATTERN = re.compile(r"\]\((src/.+?)/README(?:\.zh)?\.md\)")
TRACING_LESSONS = {
"src/30-sslsniff",
"src/31-goroutine",
"src/33-funclatency",
"src/37-uprobe-rust",
"src/39-nginx",
"src/40-mysql",
"src/48-energy",
}
def lesson_links(markdown: str) -> set[str]:
return set(LINK_PATTERN.findall(markdown))
def subsection(markdown: str, heading: str, next_heading: str) -> str:
return markdown.split(f"\n\n{heading}:\n\n", 1)[1].split(
f"\n\n{next_heading}:\n\n", 1
)[0]
class TocParityTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.english = generate_toc(ROOT / "src", ROOT, ROOT)
cls.chinese = generate_toc_cn(ROOT / "src", ROOT, ROOT)
def test_english_and_chinese_indexes_contain_the_same_lessons(self):
self.assertSetEqual(lesson_links(self.english), lesson_links(self.chinese))
def test_chinese_tracing_subsection_contains_all_tracing_lessons(self):
english_tracing = lesson_links(subsection(self.english, "Tracing", "Security"))
chinese_tracing = lesson_links(subsection(self.chinese, "追踪", "安全"))
self.assertSetEqual(english_tracing, TRACING_LESSONS)
self.assertSetEqual(chinese_tracing, TRACING_LESSONS)
if __name__ == "__main__":
unittest.main()