记一次修复 argparse 模块(或模块化一个库)

0. 起因

前段时间发现了一个好东西,Are We Modules Yet?,这个网站跟踪记录了很多库以及它们的模块化进度。一边下定决心要在上面早日留下自己的库,一边发现了一个很好的库叫做 argparse。

它有多好呢?它在 23 年底就模块化了,而且有官方提供的 xmake.lua。

不过可惜的是,xmake-repo/argparse 既没有用到 argparse/xmake.lua,也没有提供模块支持。

所以,我出手了。

所以,我出手了

-1. 更久之前

实际上四个月前我就发现了,并且给出了一个 workround,但它很丑,而且不方便,只能留着自己用。

适逢 FTXUI 也模块化了,而且手上有个马上开工的项目也要用到 argparse,我兴奋地决定给 argparse 也开开刀。

1. 利用 argparse/xmake.lua

第一步是先把 argparse/xmake.lua 利用起来。

我先开了一个本地包,把 xmake-repo/argparse/xmake.lua 复制过来:

package("argparse")
    set_kind("library", {headeronly = true})
    set_homepage("https://github.com/p-ranav/argparse")
    set_description("A single header argument parser for C++17")
    set_license("MIT")

    add_urls("https://github.com/p-ranav/argparse/archive/refs/tags/v$(version).zip",
             "https://github.com/p-ranav/argparse.git")
    add_versions("3.2", "14c1a0e975d6877dfeaf52a1e79e54f70169a847e29c7e13aa7fe68a3d0ecbf1")
    add_versions("3.1", "3e5a59ab7688dcd1f918bc92051a10564113d4f36c3bbed3ef596c25e519a062")
    add_versions("3.0", "674e724c2702f0bfef1619161815257a407e1babce30d908327729fba6ce4124")
    add_versions("2.6", "ce4e58d527b83679bdcc4adfa852af7ec9df16b76c11637823ef642cb02d2618")
    add_versions("2.7", "58cf098fd195895aeb9b9120d96f1e310994b2f44d72934c438ec91bf2191f46")
    add_versions("2.8", "9381b9ec2bdd2a350d1a0bf96d631969e3fda0cf2696e284d1359b5ee4ebb465")
    add_versions("2.9", "55396ae05d9deb8030b8ad9babf096be9c35652d5822d8321021bcabb25f4b72")

    on_install(function (package)
        os.cp("include/argparse/argparse.hpp", package:installdir("include/argparse"))
    end)

    on_test(function (package)
        assert(package:check_cxxsnippets({test = [[
            void test() {
                argparse::ArgumentParser test("test");
            }
        ]]}, {configs = {languages = "c++17"}, includes = "argparse/argparse.hpp"}))
    end)

首先呢因为模块化之后它不再是一个 headeronly 了,所以我们先把 set_kind 那里的 , {headeronly = true} 删了。也可能更好的做法是判断一下 config(在后文),现在先不管先了。

然后我们去翻阅一下 argparse/xmake.lua:

set_xmakever("2.8.2")
set_project("argparse")

set_version("3.2.0", { build = "%Y%m%d%H%M" })

option("enable_module")
option("enable_std_import", { defines = "ARGPARSE_MODULE_USE_STD_MODULE" })
option("enable_tests")
option("enable_samples")

add_cxxflags(
    "-Wall",
    "-Wno-long-long",
    "-pedantic",
    "-Wsign-conversion",
    "-Wshadow",
    "-Wconversion",
    { toolsets = { "clang", "gcc" } }
)
add_cxxflags("cl::/W4")

if is_plat("windows") then
    add_defines("_CRT_SECURE_NO_WARNINGS")
end

target("argparse", function()
    set_languages("c++17")
    set_kind("headeronly")
    if get_config("enable_module") then
        set_languages("c++20")
        set_kind("static") -- static atm because of a XMake bug, headeronly doesn't generate package module metadata
    end

    add_options("enable_std_import")

    add_includedirs("include", { public = true })
    add_headerfiles("include/argparse/argparse.hpp")
    if get_config("enable_module") then
        add_files("module/argparse.cppm", { install = true })
    end
end)

-- 后面用于测试的部分省略了

注意到核心在于 enable_moduleenable_std_import 这两个选项。查阅 xmake 文档可知它是通过构建命令的选项控制的。

查阅文档可知,当制作包时我们可以用 configs 来让外界传递参数,对应的我们在本地包中加上一些东西:

package("argparse")
    set_kind("library")
    set_homepage("https://github.com/p-ranav/argparse")
    set_description("A single header argument parser for C++17")
    set_license("MIT")

    add_configs("enable_module", { default = false, type = "boolean" })
    add_configs("enable_std_import", { default = false, type = "boolean" })

    -- ...

继续查阅 xmake 文档 可以找到利用 xmake.lua 的办法,也就是 import("package.tools.xmake").install(package),而且还能传递一个第二参数来作为构建选项:

import("package.tools.xmake").install(package, {
    "--enable_module=ENABLE_MODULE",
    "--enable_std_import=ENABLE_STD_IMPORT"
})

不过,我们继续翻阅文档,可以看到另一种更好的做法。直接修改 on_install 部分:

    on_install(function (package)
        local configs = {}
        if package:config("enable_module") then
            configs.enable_module = "ENABLE_MODULE"
        end
        if package:config("enable_std_import") then
            configs.enable_std_import = "ENABLE_STD_IMPORT"
        end
        import("package.tools.xmake").install(package, configs)
    end)

本地包的 xmake.lua 的修改到此为止。测试的部分我不知道怎么改,check_cxxsnippets 似乎不能与模块一起运作。

然而,当我们在项目的 xmake.lua 中写下

add_requires("local-repo third_party")
add_requires("argparse", {
    configs = {
        enable_module = true, 
        enable_std_import = true
    }
})

时,发现根本跑不起来😡

2. 一路修到发电站

实际上是上游 argparse/xmake.lua 写错了。

修改 target 部分即可:

target("argparse", function()
    -- ...

    -- add_options("enable_std_import")
    if get_config("enable_std_import") then -- option 用错了,改成这个
        add_defines("ARGPARSE_MODULE_USE_STD_MODULE")
    end

    add_includedirs("include", { public = true })
    add_headerfiles("include/argparse/argparse.hpp")
    add_headerfiles("include/(argparse/argparse.hpp)") -- 加一行这个,不然会炸找不到头文件
    if get_config("enable_module") then
        add_files("module/argparse.cppm", { public = true, install = true }) -- 加个 public,xmake 文档里面也说了
    end
end)

中间那个 add_headerfiles("include/(argparse/argparse.hpp)") 是我控制变量试了一万次得出的结论,两个 add_headerfiles 一个都不能少。顺便吐槽一下,argparse 里面的 wrapper cppm 用的是 argparse/argparse.hpp,测试用的又是 argparse.hpp,搞得很混乱,不然也没这么多麻烦事情。

本地测试了一下,完美通过。

3. 后续处理

然后我给 argparse 发了个 PR,等合并之后我会给 xmake-repo 也发一个 PR。不过这库五个月没有动静了,前段时间的 Issue 和 PR 似乎也没有被处理,如果等不来合并可以先用我自用的 xmake-repo 顶着。

add_repositories("keqingmoe-repo https://github.com/KeqingMoe/xmake-repo.git")
add_requires("argparse fix-xmake-modules", {
    configs = {
        enable_module = true,
        enable_std_import = true
    }
})

不要忘记在 target 中使用 add_packages 哦🤩

4. 尾声

实际上我摸索的过程远比上面写的更曲折。感觉就是 xmake 其实挺难用的,只是对于 C++ 的其他所有构建系统或包管理器相对好用一些。

最后也希望 Modules 尽快普及吧!回头研究下 FTXUI,这里面也有一段故事(