Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .agents/docs/2026-07-18-add-ffmpeg-module-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# add `ffmpeg`(公开模块包,Form A)+ 收编 compat.ffmpeg 描述符流水线

日期:2026-07-18 · 关联:pkgs/f/ffmpeg.lua、tools/compat-ffmpeg/、tests/smoke_ffmpeg_module.sh
前置:#74(compat.ffmpeg 8.1.2 全源码直编)已合并;ffmpeg-m v0.0.1 已发布 tag。

## 形态判定

- **Form A 原生模块库**(同 `imgui`):上游 [mcpplibs/ffmpeg-m](https://github.com/mcpplibs/ffmpeg-m)
自带 `mcpp.toml`,描述符只声明元数据 + tag tarball 下载地址,默认查找
`<verdir>/*/mcpp.toml`。
- 包内容 = 纯 C++23 模块层(`import ffmpeg.av;` 及 7 个分库模块),FFmpeg 全部
源码经其 `compat.ffmpeg = "8.1.2"` 依赖到达消费者(全源码直编形态,零预构建)。
C API 名称与用法完全不变。
- **linux-only**(先例 `compat.x11`):compat.ffmpeg 目前仅有 linux-x86_64 configure
快照;macOS 阻塞于 mcpp#229(依赖侧 cfg 条件源)。
- license:模块层 MIT;上游经 compat.ffmpeg 为 LGPL-2.1-or-later(无 GPL/nonfree 组件)。

## CN 镜像

gitcode `mcpp-res/ffmpeg`(与 compat 上游 tarball 同仓,tag 不冲突):
release `v0.0.1`,资产 `ffmpeg-m-0.0.1.tar.gz` = GitHub
`archive/refs/tags/v0.0.1.tar.gz` 同字节,`gtc release publish` 上传,sha256 一致。

## 验证方式(imgui-module 先例)

默认命名空间(namespace = "")的包尚不能作为 workspace 成员指向本地 path index,
故沿用 `imgui-module` 的例外通道:`tests/smoke_ffmpeg_module.sh` 重播默认 index
后以 `[dependencies] ffmpeg = "0.0.1"` 消费,gcc@16.1.0,断言 libavutil 主版本、
4 个解码器存在、demuxer 数 > 300。仅 Linux(非 Linux 直接 SKIP)。
smoke 内先 `mcpp index update` 刷新沙箱包索引(nasm 自举,mcpp#232 规避;置于
重播默认 index 之前,避免刷新覆盖重播结果)。validate.yml 新增 `ffmpeg-module`
job(ubuntu-only,镜像 imgui-module 结构)。

## 描述符流水线收编(tools/compat-ffmpeg/)

`compat.ffmpeg.lua` 为生成物(329 KB,禁止手改)。生成器原在 ffmpeg-m
(`tools/gen_config.sh`/`gen_descriptor.py`),随本次 PR 迁至本仓
`tools/compat-ffmpeg/`(fetch_upstream.sh + gen_config.sh + gen_descriptor.py),
与其生成的描述符同仓维护;ffmpeg-m 只保留模块层与导出面生成器(gen_exports.py)。
迁移回归:迁移后重跑流水线,与已合并描述符 diff 仅头注释 2 行(2281 源 + 15
生成文件字节一致;host gcc 13 与原生成环境结果一致,可复现)。
注意:宿主 `cc` 若被 xlings subos 切到交叉工具链会导致 configure 失败,用
`CC=/usr/bin/gcc` 显式指定。

## FFmpeg 版本升级 SOP

1. 本仓:改 `tools/compat-ffmpeg/fetch_upstream.sh` 的 version/sha256 pin →
`sh tools/compat-ffmpeg/gen_config.sh` 重生成描述符 → 上传新上游 tarball 到
CN 镜像 → PR。
2. ffmpeg-m:bump `compat.ffmpeg` 依赖 + `tools/fetch_upstream.sh` pin →
`python3 tools/gen_exports.py` 重生成导出面 → 审阅 diff → 发新 tag →
本仓登记新版本条目。
37 changes: 37 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,40 @@ jobs:
run: |
"$MCPP" --version
timeout 1800 bash tests/smoke_imgui_module.sh

# ── Exception: the public `ffmpeg` C++23-module package (namespace "") ──
# Same default-namespace limitation as imgui-module above — validated via a
# reseeding smoke driver instead of a workspace member. Linux-only: its
# compat.ffmpeg dependency ships a linux-x86_64 configure snapshot (macOS is
# blocked on mcpp#229 dependency cfg-conditional sources).
ffmpeg-module:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
MCPP_EFFECTIVE: "0.0.96" # keep in sync with env.MCPP_VERSION
steps:
- uses: actions/checkout@v4
- name: Restore mcpp registry cache
uses: actions/cache@v4
with:
path: ~/.mcpp/registry
key: mcpp-registry-${{ runner.os }}-${{ env.MCPP_EFFECTIVE }}-${{ hashFiles('pkgs/**/*.lua', 'tests/**', '.github/workflows/validate.yml') }}
restore-keys: |
mcpp-registry-${{ runner.os }}-${{ env.MCPP_EFFECTIVE }}-
- name: Download mcpp
run: |
curl -L -fsS -o mcpp.tar.gz \
"https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-linux-x86_64.tar.gz"
tar -xzf mcpp.tar.gz
root="$PWD/mcpp-${MCPP_VERSION}-linux-x86_64"
mkdir -p "$HOME/.mcpp/registry"
cp -a "$root/registry/." "$HOME/.mcpp/registry/"
echo "MCPP=$root/bin/mcpp" >> "$GITHUB_ENV"
echo "MCPP_VENDORED_XLINGS=$root/registry/bin/xlings" >> "$GITHUB_ENV"
echo "$root/bin" >> "$GITHUB_PATH"
- name: ffmpeg module package smoke
env:
MCPP_INDEX_MIRROR: GLOBAL
run: |
"$MCPP" --version
timeout 2700 bash tests/smoke_ffmpeg_module.sh
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上

本仓收录两类包:

- **原生 mcpp 模块库**:以 C++23 模块发布、`import` 即用,包括 `mcpplibs.*`、`nlohmann.json`、`imgui`,以及由
- **原生 mcpp 模块库**:以 C++23 模块发布、`import` 即用,包括 `mcpplibs.*`、`nlohmann.json`、`imgui`、`ffmpeg`,以及由
用户基于 mcpp 开发并登记进索引的库(如 `tensorvia-cpu`)。其上游通常自带 `mcpp.toml`,描述文件(Form A)只声明
元数据与下载地址。
- **第三方 C/C++ 库(`compat`)**:其上游不提供 mcpp 支持,描述文件(Form B)内联构建信息。该类库存在
Expand All @@ -32,7 +32,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上

| 形态 | 示例 |
|------|------|
| 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) |
| 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) |
| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
| 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) · [`compat.opencv`](pkgs/c/compat.opencv.lua)(CMake) |
Expand Down
4 changes: 2 additions & 2 deletions pkgs/c/compat.ffmpeg.lua
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
-- Auto-generated by ffmpeg-m/tools/gen_descriptor.py — do not edit by hand.
-- Auto-generated by tools/compat-ffmpeg/gen_descriptor.py — do not edit by hand.
-- Recipe: FFmpeg 8.1.2 built FROM SOURCE by mcpp (no configure/make at
-- build time). Full internal-component hermetic profile, LGPL-2.1+ (configure:
-- --disable-autodetect --disable-programs --disable-doc; everything else
-- default, x86 NASM asm included — 157 .asm units, nasm resolved
-- PATH → sandbox → xlings). The config snapshot below is deterministic for
-- (linux-x86_64, this profile) and shipped as generated_files; the source
-- list is the `make -n` ground truth of the same configure run.
-- Regenerate: ffmpeg-m tools/gen_descriptor.py (see that repo's docs).
-- Regenerate: sh tools/compat-ffmpeg/gen_config.sh (this repo).
package = {
spec = "1",
namespace = "compat",
Expand Down
34 changes: 34 additions & 0 deletions pkgs/f/ffmpeg.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Form A descriptor: the public ffmpeg module package ships its own
-- mcpp.toml. mcpp's default lookup finds <verdir>/*/mcpp.toml inside
-- the GitHub source tarball wrap.
--
-- The package is the thin C++23 module layer (import ffmpeg.av / per-lib
-- ffmpeg.avcodec, …) over FFmpeg's unchanged C API; the FFmpeg sources
-- themselves arrive through its compat.ffmpeg dependency (full source
-- build, config snapshot — see pkgs/c/compat.ffmpeg.lua). Linux-only for
-- now: compat.ffmpeg carries a linux-x86_64 configure snapshot (macOS
-- blocked on mcpp#229 dependency cfg-conditional sources).
--
package = {
spec = "1",
name = "ffmpeg",
namespace = "",
description = "C++23 module package for FFmpeg (import ffmpeg.av) — full source build via compat.ffmpeg, C API unchanged",
licenses = {"MIT"}, -- module layer; upstream via compat.ffmpeg is LGPL-2.1-or-later
repo = "https://github.com/mcpplibs/ffmpeg-m",
type = "package",

xpm = {
linux = {
["0.0.1"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/ffmpeg-m/archive/refs/tags/v0.0.1.tar.gz",
CN = "https://gitcode.com/mcpp-res/ffmpeg/releases/download/v0.0.1/ffmpeg-m-0.0.1.tar.gz",
},
sha256 = "e47dc9683f62466e9bd1c962a5f6ee83fbe3670fdb20c834bd9e8ee3f30e49fb",
},
},
},

-- (no `mcpp` field -- default lookup will find <verdir>/*/mcpp.toml)
}
122 changes: 122 additions & 0 deletions tests/smoke_ffmpeg_module.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Smoke-test the public ffmpeg module package through this checkout as a local
# mcpp path index. This validates user-facing import-only consumption:
# `[dependencies] ffmpeg = "0.0.1"` → `import ffmpeg.av;`. Linux-only (the
# package's compat.ffmpeg dependency ships a linux-x86_64 config snapshot).
set -euo pipefail

if [[ "$(uname -s)" != "Linux" ]]; then
echo "SKIP: ffmpeg module package is linux-only for now."
exit 0
fi

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MCPP_BIN="${MCPP:-}"
if [[ -z "$MCPP_BIN" ]]; then
MCPP_BIN="$(command -v mcpp || true)"
fi
if [[ -z "$MCPP_BIN" || ! -x "$MCPP_BIN" ]]; then
echo "FATAL: set MCPP=/path/to/mcpp or put mcpp on PATH" >&2
exit 1
fi

TMP="$(mktemp -d)"
if [[ "${MCPP_INDEX_KEEP_SMOKE_TMP:-0}" == "1" ]]; then
echo "KEEP: $TMP"
else
trap 'rm -rf "$TMP"' EXIT
fi

if [[ -n "${MCPP_INDEX_SMOKE_MCPP_HOME:-}" ]]; then
export MCPP_HOME="$MCPP_INDEX_SMOKE_MCPP_HOME"
else
export MCPP_HOME="$TMP/mcpp-home"
fi
mkdir -p "$MCPP_HOME/registry/data/xpkgs"

USER_MCPP="${HOME}/.mcpp"
link_xpkgs() {
local src="$1"
[[ -d "$src" ]] || return 0
find "$src" -mindepth 1 -maxdepth 1 -type d | while read -r pkg; do
ln -s "$pkg" "$MCPP_HOME/registry/data/xpkgs/$(basename "$pkg")" 2>/dev/null || true
done
}
link_xpkgs "${MCPP_INDEX_SMOKE_XPKGS_DIR:-}"
link_xpkgs "$USER_MCPP/registry/data/xpkgs"
if [[ -d "$USER_MCPP/registry/data/xim-pkgindex" ]]; then
mkdir -p "$MCPP_HOME/registry/data/xim-pkgindex"
cp -a "$USER_MCPP/registry/data/xim-pkgindex/." "$MCPP_HOME/registry/data/xim-pkgindex/" 2>/dev/null || true
rm -f "$MCPP_HOME/registry/data/xim-pkgindex/.xlings-index-cache.json"
fi
if [[ -d "$USER_MCPP/registry/bin" ]]; then
mkdir -p "$MCPP_HOME/registry"
ln -s "$USER_MCPP/registry/bin" "$MCPP_HOME/registry/bin" 2>/dev/null || true
fi
if [[ -f "$USER_MCPP/config.toml" ]]; then
cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true
fi

# compat.ffmpeg carries NASM .asm sources; mcpp bootstraps nasm from the
# sandbox package index. Release archives vendor a pre-xim-pkgindex#398
# snapshot whose nasm.lua "installs" empty (mcpp#232) — refresh BEFORE
# reseeding the default index below so the update can't clobber the reseed.
"$MCPP_BIN" index update

default_index="$MCPP_HOME/registry/data/mcpplibs"
# Reseed cleanly (see smoke_imgui_module.sh for why .git is skipped).
rm -rf "$default_index"
mkdir -p "$default_index"
( cd "$ROOT" && find . -mindepth 1 -maxdepth 1 ! -name .git -exec cp -a {} "$default_index/" \; )
rm -f "$default_index/.xlings-index-cache.json"
printf 'ok\n' > "$default_index/.mcpp-index-updated"

"$MCPP_BIN" self config --mirror "${MCPP_INDEX_MIRROR:-GLOBAL}" >/dev/null

mkdir -p "$TMP/ffmpeg-module-smoke/src"
cd "$TMP/ffmpeg-module-smoke"
cat > mcpp.toml <<EOF
[package]
name = "ffmpeg-module-smoke"
version = "0.1.0"

[toolchain]
default = "${MCPP_INDEX_FFMPEG_MODULE_TOOLCHAIN:-gcc@16.1.0}"

[dependencies]
ffmpeg = "0.0.1"

[targets.ffmpeg-module-smoke]
kind = "bin"
main = "src/main.cpp"
EOF

cat > src/main.cpp <<'EOF'
import std;
import ffmpeg.av;

int main() {
unsigned v { avutil_version() };
std::println("libavutil {}.{} module package ok", v >> 16, (v >> 8) & 0xff);
if ((v >> 16) != 60u) { // libavutil major of the FFmpeg 8.1.x train
return 1;
}

for (auto name : { "h264", "hevc", "av1", "aac" }) {
const AVCodec* dec { avcodec_find_decoder_by_name(name) };
if (dec == nullptr) {
std::println("decoder {} MISSING", name);
return 2;
}
}

void* it { nullptr };
long demuxers { 0 };
while (av_demuxer_iterate(&it)) ++demuxers;
std::println("{} demuxers registered", demuxers);
return demuxers > 300 ? 0 : 3;
}
EOF

"$MCPP_BIN" build
"$MCPP_BIN" run
23 changes: 23 additions & 0 deletions tools/compat-ffmpeg/fetch_upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh
# fetch_upstream.sh — download + extract the pinned official FFmpeg tarball
# for maintainer-time regeneration (gen_config/gen_exports/gen_descriptor).
# Consumers never need this: they get FFmpeg via the compat.ffmpeg package.
#
# Prints the extracted source root on stdout.
set -eu

version="${FFMPEG_VERSION:-8.1.2}"
sha256="${FFMPEG_SHA256:-32faba5ef67340d54724941eae1425580791195312a4fd13bf6f820a2818bf22}"
cache="${XDG_CACHE_HOME:-$HOME/.cache}/ffmpeg-m"
tarball="$cache/ffmpeg-$version.tar.gz"
srcdir="$cache/ffmpeg-$version"

mkdir -p "$cache"
if [ ! -f "$tarball" ]; then
curl -L -fsS -o "$tarball" "https://ffmpeg.org/releases/ffmpeg-$version.tar.gz" >&2
fi
echo "$sha256 $tarball" | sha256sum -c - >&2
if [ ! -d "$srcdir" ]; then
tar -xzf "$tarball" -C "$cache"
fi
echo "$srcdir"
50 changes: 50 additions & 0 deletions tools/compat-ffmpeg/gen_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh
# gen_config.sh — maintainer-time descriptor pipeline for compat.ffmpeg.
#
# Fetches the pinned OFFICIAL FFmpeg tarball (fetch_upstream.sh alongside), runs
# ./configure ONCE (out-of-tree, hermetic full profile), captures every
# generated file plus the `make -n` source list, and emits the mcpp-index
# descriptor via gen_descriptor.py alongside. Consumers never run this: FFmpeg
# reaches them as the compat.ffmpeg package.
#
# Usage: tools/compat-ffmpeg/gen_config.sh [target] [out.lua]
# target default: autodetected <os>-<arch> (only linux-x86_64 supported yet)
# out.lua default: <this repo>/pkgs/c/compat.ffmpeg.lua
set -eu

target="${1:-$(uname -s | tr 'A-Z' 'a-z' | sed s/darwin/macos/)-$(uname -m | sed s/arm64/aarch64/)}"
here="$(cd "$(dirname "$0")" && pwd)"
index_root="$(cd "$here/../.." && pwd)"
version="${FFMPEG_VERSION:-8.1.2}"
sha256="${FFMPEG_SHA256:-32faba5ef67340d54724941eae1425580791195312a4fd13bf6f820a2818bf22}"
default_out="$index_root/pkgs/c/compat.ffmpeg.lua"
out="${2:-$default_out}"

src="$(sh "$here/fetch_upstream.sh")"
bld="$(mktemp -d /tmp/ffmpeg-m-cfg.XXXXXX)"
trap 'rm -rf "$bld"' EXIT

# Full-feature hermetic profile: every INTERNAL component on; nothing probed
# from the host (--disable-autodetect) so the snapshot is reproducible and the
# consumer build needs no system libraries. programs/doc are irrelevant to a
# library package.
cd "$bld"
"$src/configure" \
--cc="${CC:-cc}" \
--disable-autodetect \
--disable-programs \
--disable-doc

# ffversion.h is normally a make-time product; generate it here with a RELATIVE
# output path (an absolute path would leak into the include guard).
sh "$src/ffbuild/version.sh" "$src" libavutil/ffversion.h

# The `make -n` dry run is the ground truth for which .c/.S/.asm files this
# frozen configuration compiles (CONFIG_* gating + _select closures resolved).
make -n > make-n.log 2>/dev/null || true
grep -oE '(\.\./[^ ]+|src)/[A-Za-z0-9_/.+-]+\.(c|S|asm)\b' make-n.log \
| sed "s|^\.\./ffmpeg-$version/|src/|; s|^$src/|src/|" \
| grep '^src/' | sort -u > sources.txt

python3 "$here/gen_descriptor.py" "$bld" "$version" "$sha256" "$out"
echo "descriptor written: $out (configured on: $target)"
Loading
Loading