moonpkgconfig

    Source-aware pkg-config metadata parsing and native dependency inspection in MoonBit

    pkg-config
    native
    ffi
    build-tools
    Download zip
    Author
    Version
    0.4.0
    License
    MIT
    Last updated
    9 hours ago
    Downloads
    158

    #MoonPkgConfig

    CI

    用 MoonBit 实现的 pkg-config 元数据引擎,面向原生 FFI、构建工具和 CI。读取 .pc 文件、解析依赖、计算编译与链接参数,并把每个参数追溯到包、字段、文件位置和依赖路径。

    核心库为纯 MoonBit,支持 wasm、wasm-gc、JavaScript 和 native;解析和查询不启动 shell,也不调用外部 pkg-config。

    需要完成的工作使用入口
    在 MoonBit 中解析元数据、计算参数moon add xmyd0915/moonpkgconfig@0.4.0
    检查文件、查询依赖或接入其他语言的构建工具moonpkgconfig inspect / query / check,支持 JSON
    构建真实原生应用zlib、Expat、文件压缩工具模板
    接入已有多包工程实验性 pre-build 集成

    0.4.0 提供统一 CLI 和版本化 JSON,修复共享依赖的链接顺序,完善文件诊断、私有依赖校验和解析资源限制。升级行为见 发行说明,具体版本的归档、标签与安装结果见 发布记录。

    #安装与快速体验

    源码构建使用 moonc 0.10.20+65efd5b93、moon 0.1.20261104,并需要本机 C 编译工具。安装 MoonBit 后运行:

    git clone https://github.com/xmyd0915/moonpkgconfig.git cd moonpkgconfig moon update moon install ./cmd/moonpkgconfig moonpkgconfig query examples/valid imagekit --libs --static --json --explain moonpkgconfig inspect examples/valid/imagekit.pc --json moonpkgconfig check examples/invalid --json

    最后一条命令检查错误样本,显示文件位置并返回退出码 1。安装目录需要在 PATH 中;也可用 moon install ./cmd/moonpkgconfig --bin <目录> 指定位置。安装后能离开仓库运行,三个子命令均支持 --help。

    不安装 MoonBit 也可以使用打包的 Linux 或 Windows 可执行文件。下载入口、适用平台、许可证和校验步骤见 独立命令行下载。

    查询自己的原生安装时,显式传入 .pc 目录和包名:

    moonpkgconfig query /path/to/sdk/lib/pkgconfig zlib --modversion moonpkgconfig query /path/to/sdk/lib/pkgconfig zlib --cflags moonpkgconfig query /path/to/sdk/lib/pkgconfig zlib --libs --static --explain moonpkgconfig check /path/to/sdk/lib/pkgconfig --path /path/to/extra/pkgconfig --json

    多个目录按命令行顺序查找,同名文件由第一个目录确定。--explain 展示参数来源与依赖路径;程序集成使用 --json,保留完整参数边界。

    #在 MoonBit 中使用

    在已有模块中执行 moon add xmyd0915/moonpkgconfig@0.4.0,为下面的测试在 moon.pkg 中添加:

    import {
    "xmyd0915/moonpkgconfig" @pc,
    } for "test"

    以下是一个可运行的测试;保存为 example_test.mbt 后执行 moon test:

    test "query a native library" {
    let input =
    #|Name: codec
    #|Description: Example native library
    #|Version: 1.0
    #|Libs: -lcodec
    #|Libs.private: -lm
    let document = @pc.parse(input, source="codec.pc")
    let packages = @pc.PackageSet::new()
    assert_true(packages.add("codec", document).is_empty())
    let result = packages.collect_libs("codec", static_linking=true)
    assert_true(result.diagnostics.is_empty())
    assert_eq(result.flags[0].value, "-lcodec")
    assert_eq(result.flags[1].value, "-lm")
    assert_eq(result.flags[1].field, "Libs.private")
    assert_eq(result.flags[1].location.source, "codec.pc")
    }

    parse 保留原始值、展开值和行列位置;PackageSet 接收内存中的文档,解析公开/私有依赖、版本约束、冲突和虚拟提供者。collect_cflags / collect_libs 返回带来源的参数数组,dependency_paths 提供最短依赖路径。读取版本和变量可使用不遍历依赖的 PackageSet::select。

    核心库不访问文件系统。原生文件加载和命令行交互在适配层实现;公开接口见 MoonCakes 文档。

    #从构建工具调用

    import json import subprocess result = subprocess.run( ["moonpkgconfig", "query", "./pkgconfig", "example", "--libs", "--json"], capture_output=True, text=True, encoding="utf-8", check=False, ) report = json.loads(result.stdout) if result.returncode != 0 or report["schema_version"] != 1 or report["status"] != "ok": raise RuntimeError(report) arguments = [flag["value"] for flag in report["flags"]]

    把 arguments 作为参数数组传给编译器,不再次执行或拆分 shell 文本。JSON 位于 stdout,stderr 应保持独立;文本查询失败时诊断写入 stderr,stdout 不输出部分参数。完整结构和调用契约见 CLI JSON 接口。

    退出码含义
    0成功,或版本条件满足
    1元数据/单文件加载诊断、缺少变量或版本条件不满足
    2调用错误、搜索目录读取失败或 inspect 文件读取失败

    --exists、版本条件和所有 CLI 参数查询检查公开与私有依赖;动态 --libs 仍只输出公开链接参数。--modversion 与 --variable=名称 只验证所选根。无关坏文件不会阻断正常包查询,首个损坏文件仍保留同名优先级。

    #真实原生应用

    通用模板 用同一个 MoonBit 规划器和构建适配器接入 zlib、Expat 与文件压缩工具。它读取安装生成的 .pc,解释依赖故障,修复后计算参数并构建应用。在仓库根目录运行:

    python scripts/review-native.py

    该命令准备固定版本的静态库,在仓库外运行消费者,检查压缩文件与 Python zlib 的双向互操作,并打印可执行文件位置。需要 Python 3.9+、CMake 3.15+;Linux 需要 C 编译器和 make,Windows 需要 Visual Studio C++ 工具及 Windows SDK。依赖版本、已有安装的复用方法和平台边界见 原生接入指南。

    多包工程示例 保留已有的包配置,通过实验性 pre-build 钩子接入正常的 moon build/run/test;运行 python scripts/verify-project-integration.py 可重现。钩子接口与固定工具链绑定。另有 Ubuntu 上的 libpng → zlib 示例,验证真实私有依赖和 PNG 像素往返。

    #验证与开发

    moon version --all moon check --target all --deny-warn moon build --target all --deny-warn moon test --target all --deny-warn python scripts/verify-cli.py python scripts/verify-install.py --package

    GitHub Actions 在 Ubuntu 与 Windows 执行四目标检查、构建、测试,验证实际安装的 CLI、源码归档消费者和真实原生应用。兼容检查使用固定的 pkgconf 3.0.7,覆盖上游样本、六个项目模板、随机输入、系统安装语料及移除依赖后的失败路径;真实静态库的编译、链接和运行单独验证。

    复现命令、工具版本和每次执行的结果见 开发指南、验证记录、兼容性检查 与 性能记录。CI 报告记录对应源码提交,不用某次测试结果代表所有环境。

    #兼容范围

    • 支持 UTF-8、LF/CRLF、开头 BOM、注释、续行、按定义顺序展开的变量,以及已知字段名的 ASCII 大小写变体;保留原文与位置。变量名和未知扩展字段区分大小写。
    • 支持版本约束、Provides、Conflicts、Requires.private、Libs.private、Cflags.private 和五种参数分类。静态编译字段仅在 --static 时加入。
    • 重复定义、未定义变量和依赖环采用严格诊断;CLI 根参数只接受一个包名,不接受版本表达式。
    • 单值和每文档累计展开预算均为 1,048,576 个 UTF-16 code units;首次超限返回 PC009 并停止。解析器保留前 100 条诊断,下一条触发 PC021 并停止。这些限制不是输入文件大小或整个进程的内存上限。
    • 搜索目录、sysroot 和系统路径过滤均由调用方显式指定,不读取 PKG_CONFIG_* 环境变量。已带 sysroot 前缀的路径不会重复添加。
    • 来源行是逻辑行起始的物理行;列为合并续行并处理注释后的 Unicode 字符列。续行中间不提供精确物理跨度。
    • 不实现完整 pkgconf 兼容、Windows 自动重定位或全部片段去重。原生示例仅转换明确支持的编译选项,未知选项返回错误。

    项目采用 MIT 许可证。固定上游样本、版本、来源与各自许可证见 ORIGINS.md;后续方向见 ROADMAP.md。

    DependencyPaths

    pub struct DependencyPaths {
    root : String
    parents : Map[String, String]
    }

    A snapshot of shortest dependency routes from one root.

    Only predecessor identifiers are stored. Paths are materialized on demand, avoiding a copy of every prefix while searching a deep graph.

    DependencyPaths::path_to

    fn DependencyPaths::path_to(self : DependencyPaths, target : String) -> Array[String]

    Return a fresh shortest path, or an empty array for an unreachable package.

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    message : String
    location : Location
    } derive(Eq, Hash,
    Debug
    )

    Diagnostics are errors in the supported subset, not pkgconf exit codes.

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::hash

    fn Diagnostic::hash(self : Diagnostic) -> Int

    Diagnostic::hash_combine

    fn Diagnostic::hash_combine(Diagnostic, Hasher) -> Unit

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    Document

    pub(all) struct Document {
    entries : Array[Entry]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    A parsed document. Invalid entries are not available to subsequent queries.

    Document::as_json

    Convert a parsed document to JSON without losing raw values or source locations.

    Document::equal

    fn Document::equal(Document, Document) -> Bool

    Document::field

    fn Document::field(self : Document, name : String) -> Entry?

    Document::json_text

    fn Document::json_text(self : Document, pretty? : Bool) -> String

    Document::not_equal

    fn Document::not_equal(x : Document, y : Document) -> Bool

    Document::to_repr

    Document::validate_metadata

    fn Document::validate_metadata(self : Document, source? : String) -> Array[Diagnostic]

    Validate the required metadata of a parsed .pc document.

    Parsing remains loss-tolerant; callers opt into package-level validation before adding a document to a package set.

    Document::variable

    fn Document::variable(self : Document, name : String) -> Entry?

    Entry

    pub(all) struct Entry {
    kind : EntryKind
    name : String
    raw : String
    value : String
    location : Location
    value_location : Location
    } derive(Eq,
    Debug
    )

    Original value is retained for explaining substitutions.

    Entry::equal

    fn Entry::equal(Entry, Entry) -> Bool

    Entry::not_equal

    fn Entry::not_equal(x : Entry, y : Entry) -> Bool

    Entry::to_repr

    EntryKind

    pub(all) enum EntryKind {
    Variable
    Field
    } derive(Eq,
    Debug
    )

    EntryKind::equal

    fn EntryKind::equal(EntryKind, EntryKind) -> Bool

    EntryKind::not_equal

    fn EntryKind::not_equal(x : EntryKind, y : EntryKind) -> Bool

    FlagResult

    pub(all) struct FlagResult {
    flags : Array[ResolvedFlag]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    FlagResult::as_json

    Convert collected compiler or linker flags to JSON with their origins.

    FlagResult::deduplicate_search_paths

    fn FlagResult::deduplicate_search_paths(self : FlagResult) -> FlagResult

    Return a copy with repeated compiler and linker search paths removed.

    Both attached (-I/path, -L/path) and split (-I /path, -L /path) forms are recognized. Other flags, including repeated libraries, are kept because their order and repetition can affect linking.

    FlagResult::equal

    fn FlagResult::equal(FlagResult, FlagResult) -> Bool

    FlagResult::filter_system_search_paths

    fn FlagResult::filter_system_search_paths(self : FlagResult, include_paths : Array[String], library_paths : Array[String]) -> FlagResult

    Remove exact system -I and -L search paths.

    The caller supplies the system paths explicitly, keeping library behavior deterministic across hosts. -isystem and path prefixes are intentionally retained, matching pkgconf's system path filtering behavior.

    FlagResult::json_text

    fn FlagResult::json_text(self : FlagResult, pretty? : Bool) -> String

    FlagResult::not_equal

    fn FlagResult::not_equal(x : FlagResult, y : FlagResult) -> Bool

    FlagResult::select

    fn FlagResult::select(self : FlagResult, selection : FlagSelection) -> FlagResult

    Keep selected arguments in their original order, with origins and diagnostics.

    Split options retain their operands when both come from the same field. IncludePaths selects -I, while OtherCflags retains -isystem and other options. LibraryPaths selects -L, Libraries selects -l, and OtherLibs retains the rest. GNU linker start/end groups remain intact in OtherLibs. The returned flags are compiler arguments, including their option prefixes.

    FlagResult::shell_text

    fn FlagResult::shell_text(self : FlagResult) -> String

    Render collected arguments as one POSIX shell command fragment.

    The underlying flags array remains the portable, lossless interface. This text form is intended for copying into sh-compatible shells.

    FlagResult::with_sysroot

    fn FlagResult::with_sysroot(self : FlagResult, sysroot : String) -> FlagResult

    Prefix absolute compiler and linker search paths with a sysroot.

    Attached and split -I, -L and -isystem forms are supported. Relative paths, paths already within the sysroot and unrelated arguments are unchanged. Every rewritten flag keeps its original package, field and source location.

    FlagSelection

    pub(all) enum FlagSelection {
    IncludePaths
    OtherCflags
    LibraryPaths
    Libraries
    OtherLibs
    } derive(Eq,
    Debug
    )

    Select one of the conventional pkg-config compiler or linker flag groups.

    FlagSelection::equal

    FlagSelection::not_equal

    fn FlagSelection::not_equal(x : FlagSelection, y : FlagSelection) -> Bool

    Location

    pub(all) struct Location {
    source : String
    line : Int
    column : Int
    } derive(Eq, Hash,
    Debug
    )

    A source location. Columns count Unicode scalar values, starting at one.

    Location::equal

    fn Location::equal(Location, Location) -> Bool

    Location::hash

    fn Location::hash(self : Location) -> Int

    Location::hash_combine

    fn Location::hash_combine(Location, Hasher) -> Unit

    Location::not_equal

    fn Location::not_equal(x : Location, y : Location) -> Bool

    Location::to_repr

    Package

    pub(all) struct Package {
    id : String
    document : Document
    } derive(Eq,
    Debug
    )

    A validated package stored under the identifier used in dependency lists.

    Package::equal

    fn Package::equal(Package, Package) -> Bool

    Package::not_equal

    fn Package::not_equal(x : Package, y : Package) -> Bool

    Package::to_repr

    PackageSet

    pub(all) struct PackageSet {
    packages : Map[String, Package]
    package_ids : Array[String]
    }

    An in-memory package collection. File discovery belongs to adapters and the CLI; the core resolver accepts parsed documents so it works on every target.

    PackageSet::add

    fn PackageSet::add(self : PackageSet, id : String, document : Document, source? : String) -> Array[Diagnostic]

    Add one package after checking its identifier and required metadata. Nothing is inserted when diagnostics are returned.

    PackageSet::check_all

    fn PackageSet::check_all(self : PackageSet, include_private? : Bool, unavailable? : Map[String, Array[Diagnostic]]) -> Array[Diagnostic]

    Check every package as a possible root and return each distinct diagnostic once. Private dependencies participate by default because a directory check should also catch problems that only affect static linking. Pass unavailable as in resolve; unrelated loader failures remain the caller's responsibility because they are not roots in this package set.

    PackageSet::check_requirements

    fn PackageSet::check_requirements(self : PackageSet, requirements : Array[Requirement], source? : String) -> Array[Diagnostic]

    Check a direct dependency list against the packages currently in the set.

    PackageSet::collect_cflags

    fn PackageSet::collect_cflags(self : PackageSet, root : String, static_linking? : Bool, unavailable? : Map[String, Array[Diagnostic]]) -> FlagResult

    Collect compiler flags in dependency order, with each dependant before its dependencies and independent branches in declaration order. Static queries also include each package's Cflags.private field. unavailable retains original diagnostics for excluded dependencies, as in resolve. Private dependency failures affect compiler queries too.

    PackageSet::collect_libs

    fn PackageSet::collect_libs(self : PackageSet, root : String, static_linking? : Bool, unavailable? : Map[String, Array[Diagnostic]]) -> FlagResult

    Collect linker flags in dependant-first order. Static queries also include private dependency edges and each package's Libs.private field. Valid private dependencies can constrain the order of public libraries in dynamic output, without adding private libraries or private diagnostics. unavailable retains original diagnostics for excluded dependencies, as in resolve, following the selected public/private edge mode.

    PackageSet::dependency_path

    fn PackageSet::dependency_path(self : PackageSet, root : String, target : String, include_private? : Bool, unavailable? : Map[String, Array[Diagnostic]]) -> Array[String]

    Return the shortest dependency path from root to target.

    Public edges are always considered; private edges participate only when include_private is true. An empty array means the target is not reachable. unavailable blocks fallback through excluded exact ids, as in resolve.

    PackageSet::dependency_paths

    fn PackageSet::dependency_paths(self : PackageSet, root : String, include_private? : Bool, unavailable? : Map[String, Array[Diagnostic]]) -> DependencyPaths

    Search once when explaining multiple parameters from the same root.

    Public edges precede private edges; equal-length routes follow declaration order. Later additions to PackageSet do not change this snapshot. unavailable blocks fallback through excluded exact ids, as in resolve.

    PackageSet::get

    fn PackageSet::get(self : PackageSet, id : String) -> Package?

    PackageSet::new

    fn PackageSet::new() -> PackageSet

    PackageSet::resolve

    fn PackageSet::resolve(self : PackageSet, root : String, include_private? : Bool, source? : String, unavailable? : Map[String, Array[Diagnostic]]) -> Resolution

    Resolve the dependency graph rooted at root.

    Requires.private participates only when include_private is true. unavailable supplies original diagnostics for excluded package ids. It blocks provider fallback for those ids without introducing filesystem IO. Empty entries are ignored; a valid exact package in this set wins. Only reachable failures are returned, with identical original diagnostics once.

    PackageSet::select

    fn PackageSet::select(self : PackageSet, root : String, source? : String, unavailable? : Map[String, Array[Diagnostic]]) -> Result[Package, Array[Diagnostic]]

    Select and validate one exact package or its first matching virtual provider. Dependency and conflict fields are not traversed. Original document errors and unavailable-package diagnostics retain their locations in a copied array. source identifies missing roots or metadata with no remaining source entry.

    Requirement

    pub(all) struct Requirement {
    name : String
    op : VersionOp
    version : String
    } derive(Eq,
    Debug
    )

    A named dependency with an optional version constraint.

    Requirement::equal

    fn Requirement::equal(Requirement, Requirement) -> Bool

    Requirement::matches

    fn Requirement::matches(self : Requirement, installed_version : String) -> Bool

    Test an installed version against one parsed dependency requirement.

    Requirement::not_equal

    fn Requirement::not_equal(x : Requirement, y : Requirement) -> Bool

    Resolution

    pub(all) struct Resolution {
    order : Array[String]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    Result of resolving one root package. Dependencies precede dependants in order; diagnostics explain every branch that could not be resolved.

    Resolution::equal

    fn Resolution::equal(Resolution, Resolution) -> Bool

    Resolution::not_equal

    fn Resolution::not_equal(x : Resolution, y : Resolution) -> Bool

    ResolvedFlag

    pub(all) struct ResolvedFlag {
    value : String
    package_id : String
    field : String
    location : Location
    } derive(Eq,
    Debug
    )

    One compiler or linker argument together with its origin.

    ResolvedFlag::equal

    ResolvedFlag::not_equal

    fn ResolvedFlag::not_equal(x : ResolvedFlag, y : ResolvedFlag) -> Bool

    VersionOp

    pub(all) enum VersionOp {
    Any
    Equal
    NotEqual
    Less
    LessEqual
    Greater
    GreaterEqual
    } derive(Eq,
    Debug
    )

    VersionOp::equal

    fn VersionOp::equal(VersionOp, VersionOp) -> Bool

    VersionOp::not_equal

    fn VersionOp::not_equal(x : VersionOp, y : VersionOp) -> Bool

    compare_versions

    fn compare_versions(left : String, right : String) -> Int

    Compare two package versions using the segment rules used by pkg-config implementations: separators are ignored, numeric segments compare by magnitude, numeric segments sort after alphabetic segments, and ~ sorts before every other segment.

    The result is negative, zero, or positive when left is respectively less than, equal to, or greater than right.

    expand

    fn expand(value : String, variables : Map[String, String], location~ : Location) -> Result[String, Diagnostic]

    The map can include caller-provided builtins such as pcfiledir.

    parse

    fn parse(content : String, source? : String, pcfiledir? : String, overrides? : Map[String, String]) -> Document

    Deliberately strict about duplicate definitions and undefined variables.

    parse_requirements

    fn parse_requirements(value : String, source? : String) -> Result[Array[Requirement], Diagnostic]

    Parse Requires/Requires.private/Conflicts expressions. No version solver yet.

    split_flags

    fn split_flags(value : String, source? : String) -> Result[Array[String], Diagnostic]

    Supports quoted spaces, empty arguments and escaped characters.