diff --git a/.agents/skills/bpf-tutorial-writing-style/SKILL.md b/.agents/skills/bpf-tutorial-writing-style/SKILL.md new file mode 100644 index 0000000..e8cba32 --- /dev/null +++ b/.agents/skills/bpf-tutorial-writing-style/SKILL.md @@ -0,0 +1,70 @@ +--- +name: bpf-tutorial-writing-style +description: Repository-local prose and bilingual checklist for drafting, rewriting, or reviewing advanced bpf-developer-tutorial README.md and README.zh.md files. Use to match the readable voice of 49-hid and the useful depth of 47-cuda-events while preserving implementation facts, complete source, commands, evidence, caveats, and references. +--- + +# BPF Tutorial Writing Style + +Use this Skill for writing quality only. Use `$write-bpf-production-tutorial` for build, KVM, exact-source checks, and PR steps. + +Read all three references before drafting or reviewing: + +1. [Advanced tutorial guidelines](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/advanced-tutorial-guidelines.md) contains the complete project-provided requirements and must remain intact. +2. [Repository house style](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/repository-house-style.md) explains how to learn from existing tutorials without copying their defects. +3. [Prose and bilingual checklist](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/prose-and-bilingual-checklist.md) carries the relevant eunomia.dev writing discipline into this repository. + +Also read the English and Chinese versions of `47-cuda-events` and `49-hid` in full. Existing lessons are examples of rhythm, depth, and teaching order, not templates. Verified code, real output, primary sources, and the user's requirements remain authoritative. + +## Write for a reader + +Start with a short, truthful scene the reader can picture. Show one observable problem or useful action, then raise the technical question that the lesson will answer. Keep one example, packet, event, task, device interaction, or failure as the thread through the article, so the opening scene returns naturally when the code and output are explained. + +Treat the scene as a teaching device, not a license to write fiction. Do not invent a named operator, customer, outage, deployment, benchmark, urgency, or result. If the implementation is a bounded lab tool, make the scene a bounded lab situation and let the real mechanism make it interesting. + +Use oral English, familiar words, and connected sentences. Write natural Chinese independently from the same facts. Chinese punctuation should be restrained: prefer one or two connected sentences that carry one technical thought over a row of short, period-heavy statements. Prefer causal explanation over inventories. A paragraph should tell the reader what happens, why it happens, and what that enables. + +Prefer positive, concrete statements throughout the tutorial. Explain what the hook, program, loader, command, or test does and which scope it covers. Rewrite `not`, `do not`, `cannot`, `will not`, `不要`, `不能`, `不会`, `并非`, and `别` whenever the same fact has an accurate positive form, and remove every double negative. Keep at most one compact boundary paragraph near the end, limited to two sentences, instead of distributing failure, limitation, and safety warnings through the article. + +Keep the actor close to the action. Start important sentences with a concrete component, use active voice when ownership matters, connect short facts through cause and effect, and give `this`, `it`, `这` and `它` an unmistakable referent. Put the claim before detailed evidence, use verbs instead of abstract noun phrases, and end a sentence on the new fact the reader should remember. + +Do not write like a paper, specification, PR description, release note, test log, or compliance report. Build and KVM evidence belongs in compilation and execution, where it should support a claim instead of becoming the article's main plot. + +## Draw the relationship when prose starts carrying a graph + +Use a small technical diagram when the running example crosses at least three dependent states, branches and later rejoins, waits and retries, transfers ownership, or connects one component to several consumers. The diagram should let the reader see a relationship that would otherwise require rereading a paragraph. Keep a simple linear filter, a single hook, or a two-step command in prose. + +Build diagrams from verified code and tests. Prefer code identifiers and short labels, one clear direction, restrained color, and enough whitespace to read the figure at ordinary README width. Introduce the figure with the concrete packet, event, task, or device interaction already used in the story, then explain the decisive branch or ownership change after the image. A diagram supports the narrative; it does not replace the source walkthrough. + +Keep the editable DOT, Mermaid, or SVG source beside the rendered PNG. Use descriptive alt text and a canonical absolute GitHub target for the image. A language-neutral figure can serve both README files when code identifiers carry the flow; prose-heavy labels call for matched Chinese and English assets. + +## Keep the source readable + +Include complete core source exactly as implemented. Teach one component at a time: introduce its role, present its source inline, and immediately explain the important logic before moving on. Do not hide source in `
` and do not separate a long source dump from all of its explanation. + +Do not simplify code, insert ellipses, or rewrite comments. Present complete source in ordinary Markdown fenced code blocks without HTML synchronization markers, then verify it against the real file. + +Place one complete-source callout near the opening and link only the lesson's GitHub directory. Let the directory serve as the entry point for source, build files, fixtures, and tests, while later component sections name individual files beside their complete code blocks. + +Use canonical GitHub absolute URLs for every other Markdown link, even when the target belongs to the current lesson. Never use `./`, `../`, or another host. If no stable GitHub URL exists, leave the text unlinked. This rule overrides link patterns in older lessons and the website call to action in the advanced guidelines. + +Keep local infrastructure private. Do not name workspace paths, shared lab repositories, VM instances, copy routes, caches, or agent trace locations in a tutorial or PR description. Retain only public reproducibility facts such as architecture, kernel version and commit, configuration, commands, and captured output. + +## Keep scope honest + +Describe what the implementation actually provides. A bounded lab command is a bounded lab command. Do not inflate it into a daemon, controller, production scheduler, security product, or operational platform. + +State requirements and explain cleanup through the normal lifecycle. Collect failure conditions, limitations, and safety boundaries into one compact paragraph of at most two sentences near the end. Move low-level kernel provenance into a compact requirements or reproducibility note. + +## Final reader check + +Read the finished lesson in order once. The reader should be able to answer: + +- What concrete problem does this lesson solve? +- How does one packet or event move through kernel and user space? +- Which eBPF mechanism makes that possible? +- Which code deserves close attention? +- How do I build, run, and recognize success? +- What does the captured output prove? +- Where does the example stop? + +Revise if the reader instead encounters a code wall, repeated disclaimers, a validation transcript, dense spec-sheet paragraphs, literal Chinese translation, or a scenario larger than the tool. This reader check is a judgment pass, not a reason to add reviewers or models. diff --git a/.agents/skills/bpf-tutorial-writing-style/agents/openai.yaml b/.agents/skills/bpf-tutorial-writing-style/agents/openai.yaml new file mode 100644 index 0000000..71a8bd1 --- /dev/null +++ b/.agents/skills/bpf-tutorial-writing-style/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "BPF Tutorial Writing Style" + short_description: "Check advanced eBPF tutorial prose and structure" + default_prompt: "Use $bpf-tutorial-writing-style to draft or review this advanced eBPF tutorial in the repository house style." diff --git a/.agents/skills/bpf-tutorial-writing-style/references/advanced-tutorial-guidelines.md b/.agents/skills/bpf-tutorial-writing-style/references/advanced-tutorial-guidelines.md new file mode 100644 index 0000000..6ee81e8 --- /dev/null +++ b/.agents/skills/bpf-tutorial-writing-style/references/advanced-tutorial-guidelines.md @@ -0,0 +1,80 @@ +# Blog Guidelines for Advanced eBPF Tutorials + +This document outlines the key patterns and requirements for writing advanced eBPF tutorials. Advanced tutorials focus on complex eBPF programs, tools, or features that extend beyond basic concepts. They require a deeper understanding of eBPF and kernel interactions. + +The audience for advanced tutorials includes developers, system administrators, and security professionals with intermediate to advanced eBPF knowledge. The goal is to provide in-depth explanations of advanced eBPF topics and practical examples that readers can apply in real-world scenarios. + +The key point in tone: Using oral English, clear and simple words and short sentence, make it attractive and easy to read, do not make it like a paper. Not too much fancy words, try to be attracive. + +You should also include all the details and information I provide to you. do not simplify or change any of them, you just need to regorganize them in a more attractive way as a tutorial blog. + +## starting with a clear and descriptive title + +Begin with a clear and descriptive title: + +``` +# eBPF Tutorial by Example: [Advanced Topic] +``` + +Kick off with a brief intro to the advanced eBPF topic you're covering, and the example or tool you'll discuss. Highlight its significance and how it extends beyond basic concepts. Let readers know what they'll learn and why it's important. + +## Incroduction to the concept, tool and Background + +(Come up with a better session title) + +Provide an overview of the specific eBPF programs, tools, or features you'll discuss. Explain their purpose, use cases, and the key eBPF features or kernel events involved. Focus on aspects that are crucial for advanced understanding. + +## High-Level Code Analysis + +Dive into the kernel-mode eBPF code and user-space code, focusing on high-level concepts rather than basic syntax. + +Always include the full code as it is first. then break down the key parts. + +Try to avoid using too much list, make it more like a story. + +Follow the steps: + +1. First, introduce The overall processing logic in both kernel and user space. +2. Then break down the kernel-mode eBPF code, include + +- How the eBPF program is structured. +- Key eBPF functionalities utilized. +- How the code interacts with kernel events. + +Do not make them a list, make them some paragraphs, you can also quote some code snippets to explain the key parts of the code if needed, focus on the logic and features used in advanced eBPF development. Don't make it too long, but make sure it is informative enough and you explain everything a advanced eBPF developer wants to know. + +3. Then briefly explain the user-space code + +Aim to help readers grasp how the code works without getting bogged down in basic details. + +## Any more detailed concepts or features explanation + +If there are other information or features that are important to understand the code, you can add them here. + +## 5. Compilation and Execution + +Provide instructions on compiling and running the eBPF programs, noting any advanced configurations or dependencies. Include commands and what readers should expect as output. Typically, you can run `make` in the relevant directory in the tutorial repository to build the code. + +Include links to complete source code and resources: + +- **Repository:** + +## 6. Summary and Call to Action + +Wrap up by summarizing the key points. Emphasize the advanced concepts covered and encourage readers to apply this knowledge. Invite them to explore more examples and tutorials, as one paragraph: + +> If you'd like to dive deeper into eBPF, check out our tutorial repository at . + +## reference + +You should include the important references and resources that used in the tutorial. If this is from other sources like kernel sample or tools, make sure to include them here and clearly mention them in the tutorial. + +## Additional Guidelines + +- **Clarity:** Use simple language and short sentences to explain complex ideas. But the information should be complete and detailed. +- **Focus on Advanced Concepts:** Assume readers have basic eBPF knowledge; skip elementary explanations. +- **Engagement:** Encourage readers to think critically and engage with the material. +- **Consistency:** Keep a consistent style and formatting throughout. +- **Code Formatting:** Ensure code snippets are well-formatted and highlight key parts. Do not change or simplify any of the code and commands, keep them as they are. +- **Proofreading:** Double-check for errors and ensure technical accuracy. +- **Accessibility:** Make the content valuable for readers with advanced expertise, avoiding unnecessary simplifications. diff --git a/.agents/skills/bpf-tutorial-writing-style/references/prose-and-bilingual-checklist.md b/.agents/skills/bpf-tutorial-writing-style/references/prose-and-bilingual-checklist.md new file mode 100644 index 0000000..85458b7 --- /dev/null +++ b/.agents/skills/bpf-tutorial-writing-style/references/prose-and-bilingual-checklist.md @@ -0,0 +1,119 @@ +# Prose and bilingual checklist + +This checklist carries the relevant eunomia.dev writing discipline into advanced eBPF tutorials. It is self-contained and does not depend on a global or external Skill. + +Use it as a reader-focused editing guide, not as a reason to add review rounds. Technical fidelity is mandatory. Numeric paragraph ranges and stylistic labels are diagnostic signals that still require judgment. + +## Fidelity is the first gate + +- **Must fix:** Any code, command, captured output, number, version, requirement, limitation, negative result, cleanup behavior, reference, or scope-bearing qualifier from the supplied material is missing or changed. +- Preserve technical names and project names. Do not replace them with vague pronouns across paragraphs. +- Treat numbers as claims. State what was measured, under which conditions, and what the result does or does not prove. +- Avoid absolute words such as “always,” “never,” “cannot,” and “impossible” unless the implementation or a primary source proves the boundary. +- Do not invent output, benchmark numbers, PIDs, paths, errors, production uses, or performance conclusions. +- Reorganize for teaching, but do not simplify away technical information. During a rewrite, compare the final text against the entry version from the end backwards and account for every removed paragraph. +- Preserving a fact does not require repeating it. Put each requirement, failure behavior, cleanup rule, and limitation at the one point where it helps the reader most. + +## Voice and argument + +- Use oral, direct English with familiar words and short connected sentences. Write as an experienced developer explaining a working tool, not as a paper, merge report, release note, or test log. +- Open with a short, truthful story or concrete scenario that the implementation can reproduce. Show an observable symptom, action, packet, event, task, device interaction, or failure, then raise the technical question instead of answering everything in the first paragraph. +- Carry the same concrete thread into the mechanism, code, and output sections. The opening should pay off when the reader sees where the event goes and how the result is produced. +- Do not fabricate a persona, customer, production incident, deployment, benchmark, urgency, or result. Avoid “In this tutorial, we will explore,” generic eBPF history, product promotion, and an abstract-style contribution list. +- Let each section answer the question raised by the previous one. A useful progression is problem, background, whole-system flow, complete code, focused walkthrough, deeper concept, compilation and execution, limits, summary, and references. +- Give each paragraph one primary job. If a paragraph establishes context, explains a mechanism, reports evidence, and states a limitation at once, split it. +- Prefer active voice and concrete actors. Name the hook, BPF program, map, callback, loader, fixture, command, or kernel rather than using “it,” “this,” “the approach,” or “the result.” +- Replace spec-sheet prose with causal explanation. If three neighboring sentences can be reordered without changing the paragraph, connect each fact to what it enables or why it matters. +- Avoid fancy or promotional words, clickbait, exclamation marks, vague superlatives, hollow calls to action, and self-praise. Attraction should come from a concrete problem and a mechanism the reader can reuse. +- Do not overuse lists. Use prose for the implementation story and lists only when parallel items become easier to scan. +- Do not let provenance become the plot. Kernel commits, guest versions, CI details, and test matrices belong in a compact reproducibility note unless the lesson is specifically about them. +- Match the scenario to the tool. A small fixture, lab command, or feature demonstration can be valuable without being described as a production service. + +## Diagram decision and presentation + +- Add a diagram when it makes a real relationship visible: three or more dependent state changes, a branch/retry loop, ownership transfer, or one component feeding several later actions. +- Keep a simple linear filter, single attach point, or two-step command in prose. The figure count follows teaching value rather than article length. +- Derive every node, edge, label, and branch from implementation or test evidence. A diagram carries technical claims and receives the same fidelity review as prose. +- Prefer a reviewable DOT, Mermaid, or SVG source and commit it beside the rendered PNG. Use deterministic rendering so another agent can reproduce the asset after a code change. +- Use short labels, code identifiers, one dominant reading direction, restrained color, and text that remains legible at README width. +- Introduce the packet, event, task, or device interaction before the figure. Explain the important branch, retry, or ownership change immediately after it. +- Use descriptive alt text and a canonical absolute GitHub image target. Keep both README files aligned on the same figure and conclusion; use paired assets when language-specific labels materially help readers. +- Update the diagram in the same change whenever implementation or prose changes its state sequence, ownership, branch condition, or cleanup path. + +## Positive and concrete sentences + +- Use a positive statement of behavior or scope. Write “The loader observes one child that it creates” instead of “The loader does not monitor every exec on the system.” Write “The example implements one aggregate FIFO” instead of opening with the scheduler policies it lacks. +- Rewrite `not`, `do not`, `does not`, `cannot`, `will not`, `不要`, `不能`, `不会`, `并非` and `别` whenever an accurate positive form exists. Remove double negatives and paragraphs built from consecutive negative sentences. +- Keep the supported scope exact while using positive wording. “The example focuses on one child” describes the implemented behavior without implying that system-wide monitoring already exists. +- Collect failure conditions, unsupported behavior, limitations, and safety boundaries near the end in one paragraph of at most two sentences. The main narrative should stay with the working path. +- Start important sentences with a concrete actor such as the hook, BPF program, map, callback, loader, fixture, kernel, or command. Keep that subject close to its verb and prefer active voice when ownership or sequencing matters. +- Connect three or more short declarative sentences through cause, contrast, or sequence. A pair of short sentences can create emphasis, while a longer note-like run should become a causal paragraph. +- Give vague referents a name. Replace an unclear `this`, `it`, `they`, `这`, `它` or `这种方式` with the component or mechanism it denotes. +- Put the claim or practical meaning before a block of numbers, counters, or output. The reader should know what the evidence demonstrates before reading the evidence. +- Prefer direct verbs over nominalizations and filler. Use “the callback reads,” “the loader attaches,” and “the test verifies” instead of “performs a read,” “conducts attachment,” or “provides verification.” +- Place known context near the sentence opening and the new result near the end. This topic-to-stress order helps adjacent sentences form one path instead of a fact catalog. + +Treat this wording as a negative example: + +> 这个例子故意只做聚合 FIFO,没有 class、公平调度、ECN、burst 控制。用它做实验可以,但别往物理网卡上挂,也别当生产调度器。 + +The paragraph defines the example through missing features and repeated warnings, so the useful mechanism disappears behind negation. Rewrite it around the implemented path and a concrete learning setting: + +> 这个例子用一条聚合 FIFO 展示 skb 所有权、发送时序和 qdisc 清理,适合在 veth、TAP 或 IFB 这类受控接口上观察完整流程,更完整的调度器可以在此基础上加入 class、公平调度、ECN 和 burst 控制。 + +Apply the same repair in English. Replace a paragraph built from “no classes,” “not production,” and “do not attach” with the mechanism the reader can run, the controlled setting that fits it, and the natural extensions that follow. + +## Paragraph and sentence checks + +- English paragraphs usually work best at 40–90 words. Inspect every paragraph above 110 words. Three consecutive paragraphs above 90 words are **Must fix**. +- Chinese paragraphs usually work best at 120–260 Chinese characters. Inspect every paragraph above 320 characters. Three consecutive paragraphs above 260 characters are **Must fix**. +- The ranges are diagnostic tripwires, not quotas. Short transitions and longer indivisible mechanism explanations are allowed. +- Keep a sentence's subject near its verb. Split long modifiers, dangling introductions, and sentences that make the reader hold several clauses before reaching the action. +- Avoid weak openings such as “It is,” “There is,” and “This is” when a concrete subject is available. +- Do not stack three or more short note-like sentences. Connect cause, mechanism, and consequence. +- Avoid a pair of neighboring negative sentences when positive wording can carry the same facts. Reserve the compact final boundary paragraph for the remaining limits. +- Cut filler such as “in order to,” “utilize,” “it is important to note that,” “due to the fact that,” “in terms of,” and “with respect to.” Prefer direct verbs. +- Do not use spaced English em dashes (` — `) or Chinese em dashes (`——`) in prose. +- Do not join independent clauses with semicolons. Use two sentences or a conjunction. +- Use colons only to introduce a list, code block, or table. Rewrite claim-colon-evidence and Chinese noun-phrase-colon openings as sentences. + +## Code and evidence presentation + +- Present full core source exactly as it exists before excerpting it again for explanation. Use ordinary Markdown fenced code blocks with no HTML markers, then require the workflow Skill's read-only `--check` mode to pass against the complete source inventory. Never silently shorten functions, replace blocks with ellipses, translate comments, or “clean up” commands. +- Teach one component at a time. Introduce the file's role, present its complete source inline, and immediately explain the parts that matter before starting the next component. +- Never hide source in `
` and never place several complete files in one uninterrupted dump followed by a distant walkthrough. Long examples need stronger component boundaries and local explanations, not collapsed code. +- Explain what advanced developers need to know: program type, attach point, context restrictions, helper or kfunc semantics, map ownership, state lifetime, concurrency, verifier constraints, event/control path, error handling, and cleanup. +- Keep build and run commands copyable. Explain prerequisites before the command and interpret real output after it. +- Label volatile fields and distinguish demonstrations, functional tests, and benchmarks. +- Use one opening complete-source callout that links only to the lesson directory. Treat that directory as the entry point for implementation, build files, fixtures, and tests; name individual files as code labels next to their complete source blocks instead of listing file links in the opening. +- Use canonical GitHub absolute URLs for every Markdown link, including links to the current lesson. Reject every `./`, `../`, or non-GitHub target. If no stable GitHub URL exists, do not add a link. This repository rule overrides link examples in precedents and the advanced guideline's website call to action. +- Keep local infrastructure and agent state private. Never publish workspace paths, shared lab repository or VM names, host-to-guest copy routes, cache locations, prompt stores, or trace paths. Public reproducibility may include architecture, kernel version and commit, configuration, commands, and captured output. + +## Natural Chinese + +- Compose Chinese from the facts and paragraph role, not from English sentence order. The two languages may use different sentence and paragraph boundaries. +- Keep punctuation restrained. When several clauses explain one subject, mechanism, or consequence, connect them naturally with commas instead of turning each clause into a separate short sentence. Prefer one or two complete sentences per ordinary paragraph; do not create a choppy sequence of periods. +- Do not imitate “short English sentences” mechanically in Chinese. A cohesive Chinese sentence may carry the concept, mechanism, compatibility boundary, and motivation together when they belong to one thought. +- Still split a sentence when its subject drifts, the causal relation becomes unclear, or the reader must hold unrelated facts at once. Fewer periods are a voice preference, not permission for an unreadable run-on sentence. +- Translate ordinary concept nouns consistently. Keep proper nouns, product names, code, commands, file names, function names, and genuine terms of art in English. +- Do not begin a Chinese prose sentence with an English common noun. Avoid English clause splicing and glossary-style prose where every other concept stays in English. +- Use full-width Chinese punctuation. Put a half-width space between CJK text and Latin letters or digits, such as “64 个事件” and “eBPF 程序.” +- Write table headings in Chinese. Translate ordinary prose inside comments only when the code block is illustrative; when the block claims to be complete source, preserve it byte-for-byte in both languages. +- Read every Chinese paragraph aloud. Rewrite literal translations, English word order, and stiff phrases that a developer would not use in conversation. + +Use this paragraph as a compact Chinese voice reference. Learn its connected rhythm, restrained symbols, technical density, and direct statement of motivation; do not copy its subject matter into unrelated lessons: + +> libbpf 是一个 C/C++ 的 eBPF 用户态加载和控制库,随着内核一起分发,几乎已经成为 eBPF 用户态事实上的 API 标准,libbpf 也支持 CO-RE(Compile Once – Run Everywhere) 的解决方案,即预编译的 bpf 代码可以在不同内核版本上正常工作,而无需为每个特定内核重新编译。我们希望尽可能的保持与 libbpf 的用户态 API 以及行为一致,尽可能减少应用迁移到 Wasm (如果需要的话)的成本。 + +The sample is not a factual source. Do not copy `libbpf`, CO-RE, Wasm, API claims, compatibility claims, or any other technical content from it unless the lesson's fact pack independently requires that content. Leakage from the sample into an unrelated lesson is a **Must fix** failure. + +## Bilingual consistency + +- Keep the same macro structure, examples, complete code payload, commands, output, claims, numbers, caveats, limitations, summary, and references in both files. +- Corresponding headings should promise the same content, but they do not need literal translations. +- A near-perfect line-for-line prose translation is a review smell. Confirm that each language reads naturally even when that changes sentence or paragraph boundaries. +- When one version changes a fact, example, figure, command, code block, output, or limitation, update the other in the same pass. + +## Final reader check + +After reading in order, an intermediate or advanced eBPF developer should be able to explain the real problem, the kernel/user-space flow, why the advanced capability is needed, how the core code works, how to build and run it, what the captured output proves, and where the example stops. If the reader remembers only a feature name, a code wall, repeated caveats, or a validation transcript, revise the narrative. diff --git a/.agents/skills/bpf-tutorial-writing-style/references/repository-house-style.md b/.agents/skills/bpf-tutorial-writing-style/references/repository-house-style.md new file mode 100644 index 0000000..531e3f3 --- /dev/null +++ b/.agents/skills/bpf-tutorial-writing-style/references/repository-house-style.md @@ -0,0 +1,48 @@ +# Repository house style + +New advanced lessons should feel at home beside `src/47-cuda-events/` and `src/49-hid/`. Read both English/Chinese README pairs in full. Use `47-cuda-events` as a depth reference for an end-to-end tool and `49-hid` as the stronger reference for voice, pacing, and teaching through a concrete device problem. + +These lessons are precedents, not perfect templates. Do not copy their unsupported claims, stale version language, inconsistent punctuation, or unnecessary feature catalogs. Verified implementation facts and current project rules win whenever a precedent differs. + +Read at most one additional completed bilingual lesson when it offers a closer subsystem pattern. For networking, good candidates include `20-tc`, `41-xdp-tcpdump`, `42-xdp-loadbalancer`, and `50-tcx`. Choose by teaching need, not by directory number. + +## Build one continuous story + +Open with a concrete situation the reader can picture. Name the small tool or example and the useful result, but do not preview every version, counter, failure case, and test in the opening. + +Give enough background to explain why the hook or kernel subsystem matters. Then introduce the whole kernel/user-space path before individual functions. Keep returning to the same packet, event, device, or failure so adjacent sections feel causally connected. + +Present complete core source exactly as required by the advanced tutorial guidelines, but organize it by component. Introduce one file, show it inline, explain its important behavior, and only then move to the next file. Never hide source in `
` and never make readers cross several complete files before reaching the explanation. + +After the source, explain the BPF structure, attach point, state and ownership, relevant helpers or kfuncs, kernel interaction, user-space lifecycle, error handling, and cleanup. Spend words on advanced constraints and surprising behavior. Do not explain ordinary C syntax. + +Compilation and execution should show copyable commands, representative real output, and what that output means. Put public kernel provenance, commit IDs, and compatibility data in a compact requirements or reproducibility paragraph. Do not name local workspaces, shared lab repositories, VM instances, copy routes, caches, agent traces, or the PR's validation history. + +End with an honest boundary, a short summary, the required invitation, and primary references. + +## Lessons from the 53-egress-pacer rewrite + +The first `53-egress-pacer` draft was technically accurate but still failed as a tutorial. It: + +- packed the scenario, kernel version, algorithm, counters, conflict behavior, and test result into the opening; +- gave the reader only a few setup paragraphs before several hundred lines of source; +- repeated the same conflict, signal, cleanup, KVM, or safety caveat in the introduction, implementation, run section, and limitations; +- read like an artifact report because validation details received more emphasis than the mechanism; +- called a fixed-duration lab demo a production or operational tool without matching CLI capabilities; +- wrote Chinese by following English sentence order. + +The rewrite kept all four complete source files, commands, output, versions, failure paths, and limits. It replaced the invented production story with an honest lab scope and followed one skb through ownership and timing. Reader review then exposed a structural problem: collecting the files before their explanations made the tutorial read like a design document. The corrected method places each complete file beside its component explanation, as tutorials 47 and 49 do. + +Reuse the corrected method, not exact section counts. Shrink overloaded openings, give the reader one useful mental model, and alternate complete component source with nearby explanation. Keep each technical fact, but place it once where the reader is ready for it. + +## Preserve modern correctness + +- Use captured output and identify fields that vary across runs. +- Source version, performance, deployment, adoption, and upstream-history claims. +- State the minimum kernel, config, privilege, architecture, and hardware requirements. +- Explain at least one meaningful failure or boundary path and relevant cleanup. +- Distinguish a functional KVM run from a benchmark. +- Avoid unsupported claims such as “production-ready,” “safe,” “negligible,” or “complete visibility.” +- Keep Markdown consistent. Use full-width Chinese punctuation and spaces between CJK text and Latin letters or digits. + +English and Chinese must share the same promise, section progression, source, commands, output, facts, caveats, summary, and references. Natural expression matters more than sentence-for-sentence symmetry. diff --git a/.agents/skills/test-bpf-tutorial-kvm/SKILL.md b/.agents/skills/test-bpf-tutorial-kvm/SKILL.md new file mode 100644 index 0000000..4f4c14a --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/SKILL.md @@ -0,0 +1,118 @@ +--- +name: test-bpf-tutorial-kvm +description: Build and smoke-test bpf-developer-tutorial lessons without loading BPF programs into the host kernel. Use when creating, changing, reviewing, or debugging an eBPF tutorial and the runtime test should reuse the already-built x86 kernel under bpf-benchmark through virtme-ng/KVM. Also use for checking whether a tutorial needs a newer kernel feature. Do not use this workflow for performance benchmarking or for rebuilding the benchmark kernel. +--- + +# Test BPF Tutorials in KVM + +Compile tutorial code on the host, but run every command that loads, attaches, or exercises BPF inside a short-lived KVM guest using the kernel artifact already built by `bpf-benchmark`. + +## Safety boundary + +- Never load or attach a tutorial BPF program on the host. This includes `bpftool prog load`, loaders, `make run`, XDP/TC attachment, sched_ext registration, and LSM/cgroup attachment. +- Do not rebuild or modify `bpf-benchmark`, its kernel tree, its Git state, caches, results, or conversation/agent-history data. +- Do not silently fall back to the host kernel or to TCG. Stop if `/dev/kvm`, `vng`, or the expected benchmark kernel is unavailable. +- Treat the tutorial directory as read-only in the guest by default. Use `--rwdir` only for a path that must return guest-generated files to the host. +- Use bounded commands. The wrapper applies a host-side timeout; add a shorter guest-side `timeout` to long-running loaders when useful. +- Report a KVM run as a functional smoke test, not as benchmark-quality evidence. +- Keep this infrastructure private in public prose. A tutorial or PR description may state the architecture, kernel version and commit, configuration, commands, and captured output, but must not name `bpf-benchmark`, local paths, VM instances, mount routes, caches, or agent-state locations. + +## Default locations + +Resolve the tutorial checkout and skill from the current Git worktree: + +```bash +TUTORIAL_ROOT="$(git rev-parse --show-toplevel)" +RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh" +``` + +The helper reads `BPF_BENCHMARK_ROOT`, defaulting to a `bpf-benchmark` checkout next to the tutorial repository. Its `--cwd` defaults to the caller's current directory. + +The reused kernel is: + +```text +$BPF_BENCHMARK_ROOT/vendor/build/x86/linux/arch/x86/boot/bzImage +``` + +## Workflow + +### 1. Validate the repositories and instructions + +Read every applicable `AGENTS.md` or equivalent repository instruction before acting. + +Require a real tutorial checkout before implementation or testing. A valid checkout has lesson sources and a working Git worktree, not only a `.git` fragment. If it is incomplete, stop and explain that the checkout must be restored; do not clone over it or delete the remnant without explicit authorization. + +The top-level `bpf-benchmark` Git repository does not need to be healthy for artifact reuse. The wrapper validates the kernel image, build config, kernel source worktree, KVM device, and toolchain independently. + +Run the preflight: + +```bash +"$RUNNER" --check +``` + +### 2. Inspect before building + +Read the lesson README, Makefile, source, and existing test commands. Identify separately: + +1. a compile-only host command; +2. the userspace loader or runtime command; +3. the event or workload that proves the program works; +4. any required network, cgroup, tracefs, debugfs, or kernel-config feature. + +Use `make -n` or inspect recipes before choosing a target. A target that invokes a loader is a guest command, even if it also compiles. + +### 3. Build on the host + +Run only compile and static-generation steps on the host. Prefer the lesson's documented build target. Preserve generated `.bpf.o`, skeletons, and userspace binaries in the lesson directory so the guest sees the exact host-built artifacts. + +Do not use `sudo` for the build. Do not run a post-build loader on the host. + +### 4. Run inside the benchmark-kernel guest + +Pass the runtime command after `--`. Arguments are preserved without host re-evaluation. + +```bash +TUTORIAL_ROOT="$(git rev-parse --show-toplevel)" +RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh" +LESSON="$TUTORIAL_ROOT/src/" + +"$RUNNER" --cwd "$LESSON" -- ./loader --help +``` + +The wrapper boots the built benchmark `bzImage` with KVM, verifies the guest release, runs as guest root, mounts bpffs/debugfs/tracefs/cgroup2, prints provenance, and powers off when the command exits. It defaults to 2 vCPUs, 4G RAM, no network, and 120 seconds. + +Use `--network user` only when the test needs an external/userspace-backed interface. Use `--network loop` for a guest-local loop network. Prefer `BPF_PROG_TEST_RUN` or deterministic guest-local traffic when either is sufficient. + +Read [the tutorial test patterns](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md) for tracer, network, writable-output, and failure-diagnosis patterns. + +### 5. Verify the behavior, not only the exit code + +Capture enough evidence to connect an input event to the expected BPF output: attach confirmation, a deterministic trigger in the same guest, expected map/event output, and clean detach or process exit. Negative tests should verify the expected verifier or attach failure. + +For a new lesson, test at least: + +- host compile from a clean lesson build state when safe; +- KVM preflight; +- one successful guest runtime path; +- one relevant error or cleanup path; +- documentation commands exactly as written. + +Do not clean unrelated files or repository-wide caches to create a clean build state. + +### 6. Report provenance and limitations + +Include the guest `uname -r`, benchmark kernel source commit, kernel image SHA-256, lesson commit or dirty state, host build command, guest command, and the observed result. The wrapper prints kernel provenance automatically. + +If the tutorial requires a feature absent from the built config or kernel, report the missing symbol/helper/attach type and stop. Propose a benchmark-kernel rebuild or upgrade separately; do not perform it as a fallback. + +## Wrapper interface + +```text +run-in-kvm.sh --check +run-in-kvm.sh [--cwd PATH] [--cpus N] [--memory SIZE] + [--timeout SECONDS] [--network none|user|loop] + [--rwdir PATH]... [--append KERNEL_ARGS]... + [--dry-run] [--verbose] -- COMMAND [ARG...] +``` + +Use `--dry-run` to inspect the generated `vng`/QEMU launch without booting. `--rwdir` is an explicit trust decision because guest root can change that host path. diff --git a/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml b/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml new file mode 100644 index 0000000..6f2b6f9 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Test BPF Tutorials in KVM" + short_description: "Build locally and test with the benchmark KVM kernel" + default_prompt: "Use $test-bpf-tutorial-kvm to build this BPF tutorial locally and run its smoke test with the bpf-benchmark KVM kernel." diff --git a/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md b/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md new file mode 100644 index 0000000..c001129 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md @@ -0,0 +1,89 @@ +# BPF tutorial KVM test patterns + +Resolve the wrapper from the tutorial worktree: + +```bash +TUTORIAL_ROOT="$(git rev-parse --show-toplevel)" +RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh" +``` + +All paths below are host paths exposed at the same location in the virtme-ng guest. + +## One-shot loader or self-test + +```bash +"$RUNNER" --cwd "$LESSON" -- ./loader --self-test +``` + +Prefer a self-test that loads the BPF object, exercises a deterministic input, checks the map or return value, and detaches before exiting. + +## Long-running tracer plus a trigger + +The loader, trigger, signal, and output assertion must all occur inside the same guest: + +```bash +"$RUNNER" --cwd "$LESSON" --timeout 90 -- bash -lc ' +set -euo pipefail +out=$(mktemp) +./tracer >"$out" 2>&1 & +pid=$! +trap '\''kill -INT "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true'\'' EXIT +sleep 1 +true # replace with the deterministic traced event +sleep 1 +kill -INT "$pid" +wait "$pid" || test $? -eq 130 +trap - EXIT +grep -F "expected event" "$out" +' +``` + +Keep the host timeout longer than the guest-side lifecycle. Avoid an unbounded `timeout ./tracer` assertion because exit 124 proves only that it was killed. + +## XDP or TC + +For a userspace-backed interface and outbound guest traffic: + +```bash +"$RUNNER" --cwd "$LESSON" --network user -- bash -lc ' +set -euo pipefail +ip -brief link +./loader --interface eth0 & +pid=$! +trap '\''kill -INT "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true'\'' EXIT +ping -c 1 -W 2 10.0.2.2 +# Assert counters or emitted events here. +' +``` + +Use `--network loop` for guest-local network testing. Prefer `BPF_PROG_TEST_RUN` when the lesson is about program behavior rather than attachment mechanics. + +## sched_ext, LSM, and cgroup programs + +- Verify the exact config symbol in the printed kernel config before booting, for example `CONFIG_SCHED_CLASS_EXT=y` or `CONFIG_BPF_LSM=y`. +- Keep sched_ext registration bounded and assert that the scheduler exits or unregisters cleanly. +- Create test cgroups only inside the guest's cgroup2 mount. +- Test LSM enforcement on guest-local temporary files or processes, never on host resources mounted read-write. + +## Returning generated output + +The tutorial directory is read-only by default. Create a dedicated output directory and expose only it when a test must retain logs or generated files: + +```bash +mkdir -p "$LESSON/test-output" +"$RUNNER" --cwd "$LESSON" --rwdir "$LESSON/test-output" -- \ + ./loader --output "$LESSON/test-output/smoke.json" +``` + +Guest root can modify any `--rwdir` path. Never expose a repository root, `.git`, credentials, task histories, or agent traces just for convenience. + +## Interpreting failures + +- **Preflight fails:** repair KVM/tool/artifact availability; do not fall back to the host kernel. +- **Guest kernel mismatch:** the image and build metadata are inconsistent; rebuild or repair the benchmark artifact separately. +- **`bpftool` asks for `linux-tools-`:** the Ubuntu `/usr/sbin/bpftool` wrapper was selected. On this machine, use the real binary at `/usr/local/sbin/bpftool` after confirming it with `--version`. +- **Unknown helper, attach type, or kfunc:** compare lesson requirements with the built kernel release/config/BTF. Report a kernel gap rather than changing the host test boundary. +- **Permission error inside guest:** confirm the command is running as guest root and that bpffs/debugfs/tracefs/cgroup2 mounted. Do not add host `sudo`. +- **Read-only filesystem:** write under `/tmp`, or explicitly expose a narrow output directory with `--rwdir`. +- **Timeout 124:** inspect loader cleanup and the trigger. A timeout is not a passing tracer test. +- **Verifier rejection:** preserve the complete verifier log and distinguish an intentional negative test from a compatibility regression. diff --git a/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh b/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh new file mode 100755 index 0000000..fa51db8 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + run-in-kvm.sh --check + run-in-kvm.sh [options] -- COMMAND [ARG...] + +Build tutorial artifacts on the host, then use this wrapper only for the +runtime command that must execute in the bpf-benchmark KVM kernel. + +Options: + --check Validate KVM, vng, and the benchmark kernel, then exit. + --cwd PATH Guest working directory (default: current directory). + --cpus N Guest vCPU count (default: 2). + --memory SIZE Guest RAM accepted by vng/QEMU (default: 4G). + --timeout SECONDS Host-side wall-clock limit (default: 120). + --network MODE none, user, or loop (default: none). + --rwdir PATH Explicit host path writable by guest root; repeatable. + --append ARGS Additional kernel command-line string; repeatable. + --dry-run Print vng's QEMU command without booting. + --verbose Show the guest boot console and vng details. + -h, --help Show this help. + +Environment: + BPF_BENCHMARK_ROOT Default: bpf-benchmark next to this repository +EOF +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +require_positive_integer() { + local name=$1 value=$2 + [[ $value =~ ^[1-9][0-9]*$ ]] || die "$name must be a positive integer: $value" +} + +quote_for_posix_shell() { + local value=$1 + value=${value//\'/\'\\\'\'} + REPLY="'$value'" +} + +benchmark_root=${BPF_BENCHMARK_ROOT:-} +cwd=$PWD +cpus=2 +memory=4G +wall_timeout=120 +network=none +check_only=false +dry_run=false +verbose=false +declare -a rwdirs=() +declare -a extra_append=() + +while (($#)); do + case $1 in + --check) + check_only=true + shift + ;; + --cwd) + (($# >= 2)) || die "--cwd requires a path" + cwd=$2 + shift 2 + ;; + --cpus) + (($# >= 2)) || die "--cpus requires a value" + cpus=$2 + shift 2 + ;; + --memory) + (($# >= 2)) || die "--memory requires a value" + memory=$2 + shift 2 + ;; + --timeout) + (($# >= 2)) || die "--timeout requires seconds" + wall_timeout=$2 + shift 2 + ;; + --network) + (($# >= 2)) || die "--network requires a mode" + network=$2 + shift 2 + ;; + --rwdir) + (($# >= 2)) || die "--rwdir requires a path" + rwdirs+=("$2") + shift 2 + ;; + --append) + (($# >= 2)) || die "--append requires a kernel command-line string" + extra_append+=("$2") + shift 2 + ;; + --dry-run) + dry_run=true + shift + ;; + --verbose) + verbose=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + *) + die "unknown option before --: $1" + ;; + esac +done + +require_positive_integer "--cpus" "$cpus" +require_positive_integer "--timeout" "$wall_timeout" +[[ $memory =~ ^[1-9][0-9]*([KkMmGgTt])?$ ]] || die "invalid --memory value: $memory" +case $network in + none|user|loop) ;; + *) die "--network must be none, user, or loop: $network" ;; +esac + +for command_name in file flock git mktemp realpath sha256sum timeout vng; do + require_command "$command_name" +done + +if [[ -z $benchmark_root ]]; then + skill_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) + tutorial_root=$(git -C "$skill_dir" rev-parse --show-toplevel) + benchmark_root=$(dirname -- "$tutorial_root")/bpf-benchmark +fi + +[[ $(uname -m) == x86_64 ]] || die "the reused artifact is x86_64, host is $(uname -m)" +[[ -c /dev/kvm && -r /dev/kvm && -w /dev/kvm ]] || \ + die "/dev/kvm is not available with read/write permission; refusing a TCG fallback" + +benchmark_root=$(realpath -e -- "$benchmark_root") +kernel_source=$benchmark_root/vendor/linux-framework +kernel_build=$benchmark_root/vendor/build/x86/linux +kernel_image=$kernel_build/arch/x86/boot/bzImage +kernel_config=$kernel_build/.config +kernel_release_file=$kernel_build/include/config/kernel.release + +[[ -d $kernel_source ]] || die "kernel source directory is missing: $kernel_source" +[[ -s $kernel_image ]] || die "built benchmark kernel is missing: $kernel_image" +[[ -r $kernel_config ]] || die "kernel config is missing: $kernel_config" +[[ -r $kernel_release_file ]] || die "kernel release file is missing: $kernel_release_file" +file -- "$kernel_image" | grep -q 'Linux kernel x86 boot executable bzImage' || \ + die "artifact is not an x86 bzImage: $kernel_image" +git -C "$kernel_source" rev-parse --verify HEAD >/dev/null 2>&1 || \ + die "kernel source Git worktree is invalid: $kernel_source" + +required_config=( + CONFIG_BPF=y + CONFIG_BPF_SYSCALL=y + CONFIG_BPF_JIT=y + CONFIG_CGROUP_BPF=y + CONFIG_KPROBES=y + CONFIG_UPROBES=y + CONFIG_FTRACE=y + CONFIG_DEBUG_INFO_BTF=y +) +for symbol in "${required_config[@]}"; do + grep -qxF "$symbol" "$kernel_config" || die "required kernel config is absent: $symbol" +done + +kernel_release=$(<"$kernel_release_file") +[[ -n $kernel_release ]] || die "kernel release file is empty: $kernel_release_file" +kernel_commit=$(git -C "$kernel_source" rev-parse HEAD) +kernel_subject=$(git -C "$kernel_source" show -s --format=%s HEAD) +kernel_dirty=no +if [[ -n $(git -C "$kernel_source" status --porcelain --untracked-files=normal) ]]; then + kernel_dirty=yes +fi +kernel_sha256=$(sha256sum "$kernel_image" | awk '{print $1}') +config_sha256=$(sha256sum "$kernel_config" | awk '{print $1}') +vng_version=$(vng --version | head -n 1) + +printf '%s\n' \ + 'KVM preflight: OK' \ + "benchmark_root=$benchmark_root" \ + "kernel_image=$kernel_image" \ + "kernel_release=$kernel_release" \ + "kernel_sha256=$kernel_sha256" \ + "kernel_config_sha256=$config_sha256" \ + "kernel_source_commit=$kernel_commit" \ + "kernel_source_dirty=$kernel_dirty" \ + "kernel_source_subject=$kernel_subject" \ + "vng_version=$vng_version" + +if $check_only; then + (($# == 0)) || die "--check does not accept a guest command" + exit 0 +fi + +(($# > 0)) || die "missing guest command; place it after --" +[[ -d $cwd ]] || die "guest working directory does not exist: $cwd" +cwd=$(realpath -e -- "$cwd") + +declare -a normalized_rwdirs=() +cwd_is_rw=false +for path in "${rwdirs[@]}"; do + [[ -d $path ]] || die "--rwdir is not a directory: $path" + path=$(realpath -e -- "$path") + normalized_rwdirs+=("$path") + if [[ $path == "$cwd" ]]; then + cwd_is_rw=true + fi +done + +quote_for_posix_shell "$kernel_release" +quoted_release=$REPLY +quote_for_posix_shell "$cwd" +quoted_cwd=$REPLY +quoted_command= +for argument in "$@"; do + quote_for_posix_shell "$argument" + quoted_command+="$REPLY " +done + +guest_script=$'#!/bin/sh\nset -eu\n' +guest_script+=$'mount_if_needed() {\n' +guest_script+=$' source=$1 type=$2 target=$3\n' +guest_script+=$' mkdir -p -- "$target"\n' +guest_script+=$' grep -qs " $target " /proc/mounts || mount -t "$type" "$source" "$target"\n' +guest_script+=$'}\n' +guest_script+=$'mount_if_needed bpf bpf /sys/fs/bpf\n' +guest_script+=$'mount_if_needed debugfs debugfs /sys/kernel/debug\n' +guest_script+=$'mount_if_needed tracefs tracefs /sys/kernel/tracing\n' +guest_script+=$'mount_if_needed none cgroup2 /sys/fs/cgroup\n' +guest_script+=$'ulimit -l unlimited\n' +guest_script+="expected_release=$quoted_release"$'\n' +guest_script+=$'actual_release=$(uname -r)\n' +guest_script+=$'if [ "$actual_release" != "$expected_release" ]; then\n' +guest_script+=$' echo "ERROR: guest kernel mismatch: expected=$expected_release actual=$actual_release" >&2\n' +guest_script+=$' exit 1\n' +guest_script+=$'fi\n' +guest_script+=$'printf "guest_kernel=%s\\nguest_identity=%s\\n" "$actual_release" "$(id)"\n' +guest_script+="cd -- $quoted_cwd"$'\n' +guest_script+="exec $quoted_command"$'\n' + +lock_file=${TMPDIR:-/tmp}/test-bpf-tutorial-kvm.lock +exec {lock_fd}>"$lock_file" +flock -w 5 "$lock_fd" || die "another tutorial KVM run holds $lock_file" + +host_script_dir=$(mktemp -d /tmp/test-bpf-tutorial-kvm.XXXXXX) +host_script_path=$host_script_dir/guest-command.sh +guest_script_dir=/tmp/${host_script_dir##*/} +guest_script_path=$guest_script_dir/guest-command.sh +cleanup() { + rm -f -- "$host_script_path" + rmdir -- "$host_script_dir" 2>/dev/null || true +} +trap cleanup EXIT +printf '%s' "$guest_script" >"$host_script_path" +chmod 0555 "$host_script_path" + +declare -a vng_command=( + vng + --run "$kernel_image" + --cwd "$cwd" + --disable-monitor + --cpus "$cpus" + --memory "$memory" + --overlay-rwdir /tmp + '--qemu-opts=-machine accel=kvm' + --append 'loglevel=4 panic=30 oops=panic' + --rodir "$guest_script_dir=$host_script_dir" + --exec "$guest_script_path" +) + +if ! $cwd_is_rw; then + vng_command+=(--rodir "$cwd") +fi +for path in "${normalized_rwdirs[@]}"; do + vng_command+=(--rwdir "$path") +done +if [[ $network != none ]]; then + vng_command+=(--network "$network") +fi +for args in "${extra_append[@]}"; do + vng_command+=(--append "$args") +done +if $verbose; then + vng_command+=(--verbose) +else + vng_command+=(--quiet) +fi +if $dry_run; then + vng_command+=(--dry-run) +fi + +printf 'guest_cwd=%s\n' "$cwd" +printf 'guest_network=%s\n' "$network" +printf 'guest_command=' +printf '%q ' "$@" +printf '\n' +if ((${#normalized_rwdirs[@]})); then + printf 'WARNING: guest root has host write access to:' >&2 + printf ' %q' "${normalized_rwdirs[@]}" >&2 + printf '\n' >&2 +fi + +if $dry_run; then + "${vng_command[@]}" +else + timeout --foreground --signal=TERM --kill-after=10s "${wall_timeout}s" \ + "${vng_command[@]}" +fi diff --git a/.agents/skills/write-bpf-production-tutorial/SKILL.md b/.agents/skills/write-bpf-production-tutorial/SKILL.md new file mode 100644 index 0000000..84183c3 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/SKILL.md @@ -0,0 +1,127 @@ +--- +name: write-bpf-production-tutorial +description: Create or revise a tested bilingual bpf-developer-tutorial lesson from the implementation and real runtime evidence. Use for tutorial scope, pinned Claude Opus 4.5 authorship, README drafting, technical flow diagrams, exact source inclusion, host builds, KVM tests, supervised completion, and PR preparation. Pair with bpf-tutorial-writing-style for prose decisions. +--- + +# Write a BPF Tutorial + +Keep the workflow small. Claude Opus 4.5 writes all tutorial prose. Codex supervises source fidelity, actual completion, bilingual consistency, validation, and publication. Do not add other writers, reviewer panels, task ledgers, reviewer databases, or extra process files. + +Invoke `$bpf-tutorial-writing-style` before writing or reviewing README prose. + +## Pinned writer + +Use the exact model ID `claude-opus-4-5-20251101` through Claude Code. Never use `opus`, `latest`, a default model, a fallback model, or another provider for tutorial prose. If this exact model is unavailable, stop and report the block instead of substituting another writer. + +Use exactly one non-interactive Claude invocation for one lesson. Require Opus to edit the existing README files in place, one specific sentence or paragraph at a time and in document order. Each Edit operation must have a narrow target. It must not generate a replacement article and overwrite the file as a whole. It must preserve the existing code-block positions while revising the prose around each block, first completing natural Chinese and then matching English from the same facts. + +The initial prompt must contain the complete task, source-fidelity rules, style rules, and final audit checklist. Opus must not ask Codex or the user a question, pause for confirmation, return a partial progress report, or stop after one language or one section. It may return only after both README files have been rewritten from beginning to end and it has run its own baseline comparison and formatting checks. Do not split the main rewrite into planned paragraph batches, switch models, or let Codex fill in prose afterward. + +A typical non-interactive invocation is: + +```bash +claude -p \ + --model claude-opus-4-5-20251101 \ + --effort high \ + --permission-mode acceptEdits \ + --allowedTools 'Read,Edit,Grep,Glob,Bash(git show *),Bash(git diff *),Bash(git status *)' \ + --output-format json +``` + +Give Opus access only to the lesson, its implementation and tests, the selected precedents, these repository-local Skills, and read-only baseline inspection through `git show` and `git diff`. Tell it to edit only the English and Chinese README pair and never commit, push, or publish. + +## 1. Prepare the fact pack and technical draft + +Codex prepares the complete input before invoking Opus. Read the entry README pair, implementation, tests, Makefile, captured output, and primary upstream references. Read these bilingual precedents in full: + +- for end-to-end depth; +- for an approachable teaching voice; +- at most one closer completed lesson when it contributes a subsystem-specific pattern. + +Build a private fact pack that accounts for every mechanism, number, version, requirement, failure path, cleanup behavior, limitation, output field, and reference that the final lesson must retain. Include useful verified background from source code and primary upstream material. More context is helpful only when it is source-grounded; do not pad the pack with plausible but unverified history, adoption, performance, or production claims. + +Prepare a complete technical draft before Opus writes. For an existing lesson, the entry README pair is the initial draft: repair factual gaps and add verified missing context without polishing the voice. For a new lesson, write a direct, comprehensive bilingual technical draft from the fact pack. Completeness and traceability matter more than style at this stage. Keep the fact pack private and do not add planning files to the repository. + +Write down two private working sentences as part of the fact pack: who the reader is, and what they should be able to explain or run at the end. + +Choose an honest scope. A lesson may teach a small operational tool or a focused kernel feature. Do not call a lab demo production-ready, and do not invent a production story that the CLI, output, and tests cannot support. + +Decide once whether a technical diagram materially improves the lesson. A diagram earns its place when the reader must track at least three dependent state changes, a branch or retry loop, ownership across kernel and user space, or one source that affects several downstream components. A short linear filter or one-step attachment usually reads better as prose. + +Codex prepares technical diagrams from the verified fact pack before the Opus prose pass. Prefer a reviewable text source such as Graphviz DOT or Mermaid, or a directly maintained SVG, and commit that source beside the rendered PNG. Keep one language-neutral diagram when code identifiers carry the meaning; use paired Chinese and English assets only when prose labels are essential. Technical flowcharts come from code and tests rather than generative illustration. + +Render the asset deterministically and record a reproducible command in the fact pack. Use a descriptive image name, readable text at normal README width, restrained color, and a clear branch or ownership direction. Published Markdown uses descriptive alt text and a canonical absolute GitHub image target such as `https://github.com/eunomia-bpf/bpf-developer-tutorial/raw/main/src//.png`. Give Opus the finished asset and source so it can introduce the diagram, connect it to the running example, and explain the important branch immediately afterward. + +## 2. Verify the implementation + +Build on the host. Use `$test-bpf-tutorial-kvm` for load, attach, and runtime behavior when the host kernel cannot run the feature safely. Exercise the normal path, one meaningful failure or boundary path, and cleanup. Record the commands, guest kernel version and commit, and real output needed by the README. + +Do not turn the README into a validation transcript. Runtime provenance supports the tutorial, but the reader-facing mechanism remains the story. + +## 3. Give the complete pack to Opus + +Give pinned Opus the entry pair, the complete technical draft, the fact pack, implementation and evidence, and the project-provided advanced tutorial guidelines through `$bpf-tutorial-writing-style`. The initial prompt must explicitly separate authoritative lesson facts from voice-only examples. Opus may reorganize and rewrite the supplied facts, but it must not introduce technical content from a style sample or precedent unless that content already appears in the fact pack. + +The lesson must start from one short, truthful scene that the implementation can reproduce, then follow one packet, event, task, device interaction, or failure through the whole kernel/user-space path. Use the scene to raise the technical question before naming every mechanism, requirement, counter, and limitation. Never invent a customer, outage, production deployment, performance result, or tool capability to make the opening sound important. + +Include the complete core source exactly as implemented, then walk through the important mechanisms. Tell Opus explicitly to use repeated local edits in document order and to continue without returning until the last paragraph of the second language is complete. Replacing the complete README in one write is a workflow failure even when the final prose looks acceptable. + +Follow the selected precedent's component rhythm. Introduce one component, present its complete source inline, and explain its important logic before moving to the next component. Do not hide source in `
` or collect every file into one uninterrupted source dump. The source must remain searchable, copyable, and byte-exact. + +Explain advanced eBPF behavior, not ordinary C syntax. Keep build, run, expected output, requirements, summary, and primary references. Explain cleanup as part of the normal mechanism instead of turning it into a warning section. Compress failure conditions, limitations, and safety boundaries into one short paragraph of at most two sentences near the end. + +Use one opening complete-source callout whose only target is the lesson directory at `https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/`. The directory is the single entry point for implementation files, the `Makefile`, fixtures, and tests; introduce individual filenames as code labels beside their complete blocks instead of turning the opening into a file-link catalog. + +Use canonical GitHub absolute URLs for every other Markdown link. Never publish a `./` or `../` target or a link to another host. If no stable GitHub URL exists, omit the link. This repository rule overrides link examples in older lessons and the advanced guideline's website call to action. + +Do not publish local infrastructure details. Keep workspace paths, shared lab repository names, VM names, host-to-guest copy steps, cache locations, and agent trace paths out of the README and PR description. Public reproducibility text may state the architecture, kernel version and commit, required configuration, commands, and captured output. + +Opus must write Chinese as Chinese, using the voice sample and rules in `$bpf-tutorial-writing-style`, then write natural English with the same promise, structure, source, commands, evidence, and limits. It must not translate sentence by sentence. It must finish both files in the same lesson session. + +## 4. Supervise actual completion + +Do not accept Opus's final message as evidence that the work is complete. Inspect the files and diff. Confirm that both README files changed, every requested prose paragraph was actually rewritten, every source file still appears exactly once, and code, commands, captured output, versions, requirements, failure behavior, cleanup, limitations, and references remain intact. + +For a full rewrite, compare prose outside fenced blocks with the entry version. Unchanged titles, tables, commands, output, and reference entries may be intentional. Unchanged explanatory paragraphs are evidence that the rewrite is incomplete unless the user explicitly exempted them. Inspect diff hunk distribution as well: changes concentrated only at the opening or ending do not satisfy a paragraph-by-paragraph rewrite. + +When the lesson contains a diagram, inspect the rendered image as well as its source. Confirm that every node and edge follows the implementation, both languages introduce the same figure and draw the same conclusion, the PNG remains legible at README width, and the absolute GitHub image target points to the committed asset. + +Codex performs this postcondition check after the one complete rewrite. If it finds a concrete defect, reject Opus's self-report and send one exact defect list back to the same pinned-model session. Opus must fix every listed issue without asking questions and rerun the final checks. This supervised correction is not permission to draft the lesson in batches: the first invocation must still attempt the entire Chinese and English pair before returning. + +Codex may point out omissions, contradictions, awkward passages, broken links, or failed checks, but Opus must make every reader-facing prose revision. After the supervised Opus correction, Codex may apply a deterministic non-prose correction such as canonicalizing a verified link target, normalizing line endings, or restoring a source block byte-for-byte from its real file. It must not use this exception to rewrite wording, repair missing explanation, or introduce facts. + +Preserve the model response, failed run, partial diff, prompt, and session history. Never delete, truncate, overwrite, or clean real conversation history, agent traces, prompts, partial runs, or failed runs. + +## 5. Run one reader review + +Read both files from top to bottom as an intermediate eBPF developer. Fix the text when: + +- the opening reads like an abstract, specification, PR description, or test report; +- a large code wall arrives before the reader has a useful mental model; +- setup, conflicts, signals, KVM provenance, or limitations interrupt the main mechanism; +- a section catalogs facts without explaining cause and effect; +- the scenario promises a tool more capable than the implementation; +- Chinese follows English word order or switches languages unnecessarily; +- the reader reaches the end remembering only a feature name or validation transcript. + +For a rewrite, compare the finished pair with the entry version once. Confirm that code, commands, captured output, versions, requirements, cleanup, the compact final boundary, and primary references remain accurate. Failure and limitation details may be condensed into the required two-sentence ending instead of surviving as a catalog. Every other removed passage should be repetition, stale framing, or material that moved to a better place. Word counts, heading counts, and repeated-term counts can expose bloat, but they are diagnostics rather than targets. + +This is the normal review gate. Keep the pinned Opus session as the only prose editor. Codex reports concrete defects and reruns deterministic checks; Opus revises the affected paragraphs. The exact model identity is required by the workflow, but it is not evidence of quality. Only the finished artifact and passing checks are evidence. + +## 6. Validate and publish + +Check every complete-source block in the English and Chinese pair: + +```bash +python3 .agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py \ + --repo "$(git rev-parse --show-toplevel)" \ + --readme src//README.md \ + --readme src//README.zh.md \ + --expected-source src//.bpf.c \ + --expected-source src//.c \ + --check +``` + +Repeat `--expected-source` for each complete shared header. Use ordinary Markdown fenced code blocks without HTML markers. Copy complete source from the real file, then run the checker; it is deliberately read-only. + +Run `git diff --check`, the lesson build, the relevant host tests, the KVM runtime test, and repository documentation checks. Re-render every diagram from its committed text or vector source and confirm that the generated PNG matches the committed asset. Inspect the final diff for unintended files. Commit and push the repository-local Skills and tutorial changes only after these checks pass. Do not merge unless the user asks. diff --git a/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml b/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml new file mode 100644 index 0000000..00427e2 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "BPF Tutorial Writer" + short_description: "Write and test clear bilingual eBPF lessons" + default_prompt: "Use $write-bpf-production-tutorial to turn this eBPF example into a clear, tested bilingual tutorial." diff --git a/.agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py b/.agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py new file mode 100755 index 0000000..b3e2da0 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Check exact source files embedded in standard Markdown code fences.""" + +from __future__ import annotations + +import argparse +import collections +import pathlib +import re +import sys + + +FENCE_RE = re.compile(r"^(`{3,})([A-Za-z0-9_+.-]*)[ \t]*$") +LEGACY_MARKERS = ("") +LANGUAGES = { + ".c": "c", + ".cc": "cpp", + ".cpp": "cpp", + ".go": "go", + ".h": "c", + ".py": "python", + ".rs": "rust", + ".sh": "bash", +} + + +def contained_path(root: pathlib.Path, value: str, label: str) -> pathlib.Path: + path = (root / value).resolve() + try: + path.relative_to(root) + except ValueError as error: + raise ValueError(f"{label} escapes repository root: {value}") from error + return path + + +def parse_fenced_blocks(readme: pathlib.Path) -> list[tuple[str, str]]: + text = readme.read_text(encoding="utf-8") + for marker in LEGACY_MARKERS: + if marker in text: + raise ValueError( + f"legacy complete-source HTML marker is not allowed in {readme.name}" + ) + + lines = text.splitlines(keepends=True) + blocks: list[tuple[str, str]] = [] + index = 0 + while index < len(lines): + opening = FENCE_RE.match(lines[index].rstrip("\r\n")) + if not opening: + index += 1 + continue + + fence, language = opening.groups() + end = index + 1 + while end < len(lines) and lines[end].rstrip("\r\n") != fence: + end += 1 + if end == len(lines): + raise ValueError(f"unclosed Markdown code fence in {readme.name}") + blocks.append((language, "".join(lines[index + 1 : end]))) + index = end + 1 + + return blocks + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True, help="absolute repository root") + parser.add_argument( + "--readme", action="append", required=True, help="repository-relative README" + ) + parser.add_argument( + "--expected-source", + action="append", + required=True, + help="repository-relative complete core source; repeat for the full inventory", + ) + parser.add_argument( + "--check", action="store_true", required=True, help="check exact fenced source" + ) + return parser.parse_args() + + +def resolve_readmes( + root: pathlib.Path, values: list[str] +) -> list[tuple[str, pathlib.Path]]: + readmes: list[tuple[str, pathlib.Path]] = [] + seen: set[pathlib.Path] = set() + for value in values: + readme = contained_path(root, value, "README path") + if not readme.is_file(): + raise ValueError(f"README does not exist: {value}") + if readme in seen: + raise ValueError(f"duplicate README path: {value}") + readmes.append((value, readme)) + seen.add(readme) + if len(readmes) != 2: + raise ValueError("pass exactly the English and Chinese README pair") + names = {path.name for _, path in readmes} + parents = {path.parent for _, path in readmes} + if names != {"README.md", "README.zh.md"} or len(parents) != 1: + raise ValueError("README paths must be README.md and README.zh.md from one lesson") + return readmes + + +def source_payload( + root: pathlib.Path, value: str, readme_paths: set[pathlib.Path] +) -> tuple[str, str]: + source = contained_path(root, value, "expected source path") + if not source.is_file(): + raise ValueError(f"expected source file does not exist: {value}") + if source in readme_paths: + raise ValueError(f"expected source cannot be a tutorial README: {value}") + if source.suffix not in LANGUAGES: + raise ValueError(f"unsupported expected-source file type: {value}") + + source_bytes = source.read_bytes() + if not source_bytes: + raise ValueError(f"complete source cannot be empty: {value}") + if not source_bytes.endswith(b"\n"): + raise ValueError(f"complete source must end with an LF byte: {value}") + source_text = source_bytes.decode("utf-8") + if "\r" in source_text: + raise ValueError(f"complete source must use LF line endings: {value}") + return LANGUAGES[source.suffix], source_text + + +def check_readme( + value: str, + readme: pathlib.Path, + expected: collections.Counter[tuple[str, str]], + labels: dict[tuple[str, str], list[str]], +) -> None: + actual = collections.Counter(parse_fenced_blocks(readme)) + for payload, count in expected.items(): + found = actual[payload] + names = ", ".join(labels[payload]) + if found < count: + raise ValueError(f"missing exact fenced source in {value}: {names}") + if found > count: + raise ValueError(f"duplicate exact fenced source in {value}: {names}") + + +def main() -> int: + args = parse_arguments() + root = pathlib.Path(args.repo).resolve() + if not (root / ".git").exists(): + print(f"not a Git worktree: {root}", file=sys.stderr) + return 2 + + try: + readmes = resolve_readmes(root, args.readme) + readme_paths = {path for _, path in readmes} + labels: dict[tuple[str, str], list[str]] = collections.defaultdict(list) + expected: collections.Counter[tuple[str, str]] = collections.Counter() + seen_values: set[str] = set() + for value in args.expected_source: + if value in seen_values: + raise ValueError(f"duplicate expected source: {value}") + payload = source_payload(root, value, readme_paths) + expected[payload] += 1 + labels[payload].append(value) + seen_values.add(value) + + for value, readme in readmes: + check_readme(value, readme, expected, labels) + print(f"checked {sum(expected.values())} fenced source blocks in {value}") + except (OSError, UnicodeError, ValueError) as error: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py b/.agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py new file mode 100755 index 0000000..8fb9854 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Tests for marker-free tutorial source-block checks.""" + +from __future__ import annotations + +import pathlib +import subprocess +import tempfile +import unittest + + +SCRIPT = pathlib.Path(__file__).with_name("sync-source-blocks.py") + + +class SourceBlocksTest(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = pathlib.Path(self.temporary.name) + (self.root / ".git").mkdir() + (self.root / "src/lesson").mkdir(parents=True) + (self.root / "src/lesson/tool.c").write_text( + "int main(void)\n{\n\treturn 0;\n}\n", encoding="utf-8" + ) + (self.root / "src/lesson/contract.h").write_text( + "#define VALUE 1\n", encoding="utf-8" + ) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def write_readme( + self, + name: str, + payload: str | None = None, + language: str = "c", + fence: str = "```", + ) -> pathlib.Path: + if payload is None: + payload = (self.root / "src/lesson/tool.c").read_text(encoding="utf-8") + path = self.root / name + path.write_text( + f"# Lesson\n\n{fence}{language}\n{payload}{fence}\n", encoding="utf-8" + ) + return path + + def write_pair(self, **kwargs: str) -> None: + self.write_readme("src/lesson/README.md", **kwargs) + self.write_readme("src/lesson/README.zh.md", **kwargs) + + def run_script( + self, + *arguments: str, + expected: tuple[str, ...] = ("src/lesson/tool.c",), + ) -> subprocess.CompletedProcess[str]: + expected_arguments = [ + item for source in expected for item in ("--expected-source", source) + ] + return subprocess.run( + [ + "python3", + str(SCRIPT), + "--repo", + str(self.root), + *expected_arguments, + *arguments, + ], + text=True, + capture_output=True, + check=False, + ) + + def check_pair(self, expected: tuple[str, ...] = ("src/lesson/tool.c",)): + return self.run_script( + "--readme", + "src/lesson/README.md", + "--readme", + "src/lesson/README.zh.md", + "--check", + expected=expected, + ) + + def test_accepts_exact_standard_markdown_fences(self) -> None: + self.write_pair() + result = self.check_pair() + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("checked 1 fenced source blocks", result.stdout) + + def test_rejects_stale_source_block(self) -> None: + self.write_pair(payload="int stale;\n") + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("missing exact fenced source", result.stderr) + + def test_rejects_duplicate_exact_source_block(self) -> None: + source = (self.root / "src/lesson/tool.c").read_text(encoding="utf-8") + payload = f"{source}```\n\n```c\n{source}" + self.write_pair(payload=payload) + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("duplicate exact fenced source", result.stderr) + + def test_rejects_legacy_html_markers(self) -> None: + self.write_pair() + readme = self.root / "src/lesson/README.md" + readme.write_text( + readme.read_text(encoding="utf-8") + + "\n", + encoding="utf-8", + ) + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("HTML marker is not allowed", result.stderr) + + def test_rejects_source_outside_repository(self) -> None: + self.write_pair() + result = self.check_pair(expected=("../outside.c",)) + self.assertEqual(1, result.returncode) + self.assertIn("escapes repository root", result.stderr) + + def test_rejects_readme_as_source(self) -> None: + self.write_pair() + result = self.check_pair(expected=("src/lesson/README.md",)) + self.assertEqual(1, result.returncode) + self.assertIn("expected source cannot be a tutorial README", result.stderr) + + def test_rejects_incomplete_inventory(self) -> None: + self.write_pair() + result = self.check_pair( + expected=("src/lesson/tool.c", "src/lesson/contract.h") + ) + self.assertEqual(1, result.returncode) + self.assertIn("src/lesson/contract.h", result.stderr) + + def test_rejects_unsupported_source_type(self) -> None: + source = self.root / "src/lesson/source.txt" + source.write_text("text\n", encoding="utf-8") + self.write_pair(payload="text\n", language="text") + result = self.check_pair(expected=("src/lesson/source.txt",)) + self.assertEqual(1, result.returncode) + self.assertIn("unsupported expected-source file type", result.stderr) + + def test_accepts_longer_fence_for_source_with_backticks(self) -> None: + source = self.root / "src/lesson/tool.py" + payload = 'value = """\n```\n"""\n' + source.write_text(payload, encoding="utf-8") + self.write_pair(payload=payload, language="python", fence="````") + result = self.check_pair(expected=("src/lesson/tool.py",)) + self.assertEqual(0, result.returncode, result.stderr) + + def test_rejects_empty_source(self) -> None: + (self.root / "src/lesson/tool.c").write_bytes(b"") + self.write_pair(payload="") + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("complete source cannot be empty", result.stderr) + + def test_rejects_source_without_final_lf(self) -> None: + (self.root / "src/lesson/tool.c").write_bytes(b"int value = 1;") + self.write_pair(payload="int value = 1;\n") + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("must end with an LF byte", result.stderr) + + def test_requires_bilingual_pair(self) -> None: + self.write_readme("src/lesson/README.md") + result = self.run_script( + "--readme", "src/lesson/README.md", "--check" + ) + self.assertEqual(1, result.returncode) + self.assertIn("exactly the English and Chinese README pair", result.stderr) + + def test_rejects_unclosed_fence(self) -> None: + self.write_pair() + readme = self.root / "src/lesson/README.zh.md" + readme.write_text("# Lesson\n\n```c\nint value;\n", encoding="utf-8") + result = self.check_pair() + self.assertEqual(1, result.returncode) + self.assertIn("unclosed Markdown code fence", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 99dc7e6..b30fde7 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -14,7 +14,9 @@ jobs: steps: - uses: actions/checkout@v7 - name: Test documentation scripts - run: python3 -m unittest scripts.test_check_internal_links scripts.test_generate_toc scripts.test_generate_compatibility + run: | + python3 -m unittest scripts.test_check_internal_links scripts.test_generate_toc scripts.test_generate_compatibility + python3 .agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py - name: Regenerate documentation indexes run: | python3 scripts/generate_toc.py diff --git a/.github/workflows/test-libbpf.yml b/.github/workflows/test-libbpf.yml index c90df60..dfa75c6 100644 --- a/.github/workflows/test-libbpf.yml +++ b/.github/workflows/test-libbpf.yml @@ -20,6 +20,7 @@ jobs: submodules: 'recursive' - name: install deps run: | + sudo apt-get update sudo apt-get install -y --no-install-recommends \ libelf1 libelf-dev zlib1g-dev \ make git clang llvm pkg-config build-essential dwarves \