V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
waruqi
V2EX  ›  程序员

xmake v2.5.9 发布,改进 C++20 模块,并支持 Nim, Keil MDK 和 Unity Build

  •  
  •   waruqi ·
    waruqi · 2021-11-01 07:26:05 +08:00 · 1396 次点击
    这是一个创建于 1144 天前的主题,其中的信息可能已经有所发展或是发生改变。

    xmake 是一个基于 Lua 的轻量级跨平台构建工具,使用 xmake.lua 维护项目构建,相比 makefile/CMakeLists.txt ,配置语法更加简洁直观,对新手非常友好,短时间内就能快速入门,能够让用户把更多的精力集中在实际的项目开发上。

    这个版本,我们增加了大量重量级的新特性,例如:Nim 语言项目的构建支持,Keil MDK ,Circle 和 Wasi 工具链支持。

    另外,我们对 C++20 Modules 进行了大改进,不仅支持最新 gcc-11, clang 和 msvc 编译器,而且还得模块间依赖做了自动分析,实现最大程度的并行化编译支持。

    最后,还有一个比较有用的特性就是 Unity Build 支持,通过它我们可以对 C++ 代码的编译速度做到很大程度的提升。

    新特性介绍

    Nimlang 项目构建

    最近,我们新增了对 Nimlang 项目的构建支持,相关 issues 见:#1756

    创建空工程

    我们可以使用 xmake create 命令创建空工程。

    xmake create -l nim -t console test
    xmake create -l nim -t static test
    xmake create -l nim -t shared test
    

    控制台程序

    add_rules("mode.debug", "mode.release")
    
    target("test")
        set_kind("binary")
        add_files("src/main.nim")
    
    $ xmake -v
    [ 33%]: linking.release test
    /usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache -o:b
    uild/macosx/x86_64/release/test src/main.nim
    [100%]: build ok!
    

    静态库程序

    add_rules("mode.debug", "mode.release")
    
    target("foo")
        set_kind("static")
        add_files("src/foo.nim")
    
    target("test")
        set_kind("binary")
        add_deps("foo")
        add_files("src/main.nim")
    
    $ xmake -v
    [ 33%]: linking.release libfoo.a
    /usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
    :staticlib --noMain --passC:-DNimMain=NimMain_B6D5BD02 --passC:-DNimMainInner=NimMainInner_B6D5B
    D02 --passC:-DNimMainModule=NimMainModule_B6D5BD02 --passC:-DPreMain=PreMain_B6D5BD02 --passC:-D
    PreMainInner=PreMainInner_B6D5BD02 -o:build/macosx/x86_64/release/libfoo.a src/foo.nim
    [ 66%]: linking.release test
    /usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
    ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
    [100%]: build ok!
    

    动态库程序

    add_rules("mode.debug", "mode.release")
    
    target("foo")
        set_kind("shared")
        add_files("src/foo.nim")
    
    target("test")
        set_kind("binary")
        add_deps("foo")
        add_files("src/main.nim")
    
    $ xmake -rv
    [ 33%]: linking.release libfoo.dylib
    /usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
    :lib --noMain -o:build/macosx/x86_64/release/libfoo.dylib src/foo.nim
    [ 66%]: linking.release test
    /usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
    ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
    [100%]: build ok!
    

    C 代码混合编译

    add_rules("mode.debug", "mode.release")
    
    target("foo")
        set_kind("static")
        add_files("src/*.c")
    
    target("test")
        set_kind("binary")
        add_deps("foo")
        add_files("src/main.nim")
    

    Nimble 依赖包集成

    完整例子见:Nimble Package Example

    add_rules("mode.debug", "mode.release")
    
    add_requires("nimble::zip >0.3")
    
    target("test")
        set_kind("binary")
        add_files("src/main.nim")
        add_packages("nimble::zip")
    

    main.nim

    import zip/zlib
    
    echo zlibVersion()
    

    Native 依赖包集成

    完整例子见:Native Package Example

    add_rules("mode.debug", "mode.release")
    
    add_requires("zlib")
    
    target("test")
        set_kind("binary")
        add_files("src/main.nim")
        add_packages("zlib")
    

    main.nim

    proc zlibVersion(): cstring {.cdecl, importc}
    
    echo zlibVersion()
    

    Unity Build 加速

    我们知道,C++ 代码编译速度通常很慢,因为每个代码文件都需要解析引入的头文件。

    而通过 Unity Build ,我们通过将多个 cpp 文件组合成一个来加速项目的编译,其主要好处是减少了解析和编译包含在多个源文件中的头文件内容的重复工作,头文件的内容通常占预处理后源文件中的大部分代码。

    Unity 构建还通过减少编译链创建和处理的目标文件的数量来减轻由于拥有大量小源文件而导致的开销,并允许跨形成统一构建任务的文件进行过程间分析和优化(类似于效果链接时优化)。

    它可以极大提升 C/C++ 代码的编译速度,通常会有 30% 的速度提升,不过根据项目的复杂程度不同,其带来的效益还是要根据自身项目情况而定。

    xmake 在 v2.5.9 版本中,也已经支持了这种构建模式。相关 issues 见 #1019

    如何启用?

    我们提供了两个内置规则,分别处理对 C 和 C++ 代码的 Unity Build 。

    add_rules("c.unity_build")
    add_rules("c++.unity_build")
    

    Batch 模式

    默认情况下,只要设置上述规则,就会启用 Batch 模式的 Unity Build ,也就是 xmake 自动根据项目代码文件,自动组织合并。

    target("test")
        set_kind("binary")
        add_includedirs("src")
        add_rules("c++.unity_build", {batchsize = 2})
        add_files("src/*.c", "src/*.cpp")
    

    我们可以额外通过设置 {batchsize = 2} 参数到规则,来指定每个合并 Batch 的大小数量,这里也就是每两个 C++ 文件自动合并编译。

    编译效果大概如下:

    $ xmake -r
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_642A245F.cpp
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_bar.cpp
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_73161A20.cpp
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_F905F036.cpp
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_foo.cpp
    [ 11%]: ccache compiling.release build/.gens/test/unity_build/main.c
    [ 77%]: linking.release test
    [100%]: build ok
    

    由于我们仅仅启用了 C++ 的 Unity Build ,所以 C 代码还是正常挨个编译。另外在 Unity Build 模式下,我们还是可以做到尽可能的并行编译加速,互不冲突。

    如果没有设置 batchsize 参数,那么默认会吧所有文件合并到一个文件中进行编译。

    Group 模式

    如果上面的 Batch 模式自动合并效果不理想,我们也可以使用自定义分组,来手动配置哪些文件合并到一起参与编译,这使得用户更加地灵活可控。

    target("test")
        set_kind("binary")
        add_rules("c++.unity_build", {batchsize = 0}) -- disable batch mode
        add_files("src/*.c", "src/*.cpp")
        add_files("src/foo/*.c", {unity_group = "foo"})
        add_files("src/bar/*.c", {unity_group = "bar"})
    

    我们使用 {unity_group = "foo"} 来指定每个分组的名字,以及包含了哪些文件,每个分组的文件都会单独被合并到一个代码文件中去。

    另外,batchsize = 0 也强行禁用了 Batch 模式,也就是说,没有设置 unity_group 分组的代码文件,我们还是会单独编译它们,也不会自动开启自动合并。

    Batch 和 Group 混合模式

    我们只要把上面的 batchsize = 0 改成非 0 值,就可以让分组模式下,剩余的代码文件继续开启 Batch 模式自动合并编译。

    target("test")
        set_kind("binary")
        add_includedirs("src")
        add_rules("c++.unity_build", {batchsize = 2})
        add_files("src/*.c", "src/*.cpp")
        add_files("src/foo/*.c", {unity_group = "foo"})
        add_files("src/bar/*.c", {unity_group = "bar"})
    

    忽略指定文件

    如果是 Batch 模式下,由于是自动合并操作,所以默认会对所有文件执行合并,但如果有些代码文件我们不想让它参与合并,那么我们也可以通过 {unity_ignored = true} 去忽略它们。

    target("test")
        set_kind("binary")
        add_includedirs("src")
        add_rules("c++.unity_build", {batchsize = 2})
        add_files("src/*.c", "src/*.cpp")
        add_files("src/test/*.c", {unity_ignored = true}) -- ignore these files
    

    Unique ID

    尽管 Unity Build 带啦的收益不错,但是我们还是会遇到一些意外的情况,比如我们的两个代码文件里面,全局命名空间下,都存在相同名字的全局变量和函数。

    那么,合并编译就会带来编译冲突问题,编译器通常会报全局变量重定义错误。

    为了解决这个问题,我们需要用户代码上做一些修改,然后配合构建工具来解决。

    比如,我们的 foo.cpp 和 bar.cpp 都有全局变量 i 。

    foo.cpp

    namespace {
        int i = 42;
    }
    
    int foo()
    {
        return i;
    }
    

    bar.cpp

    namespace {
        int i = 42;
    }
    
    int bar()
    {
        return i;
    }
    

    那么,我们合并编译就会冲突,我们可以引入一个 Unique ID 来隔离全局的匿名空间。

    foo.cpp

    namespace MY_UNITY_ID {
        int i = 42;
    }
    
    int foo()
    {
        return MY_UNITY_ID::i;
    }
    

    bar.cpp

    namespace MY_UNITY_ID {
        int i = 42;
    }
    
    int bar()
    {
        return MY_UNITY_ID::i;
    }
    

    接下来,我们还需要保证代码合并后, MY_UNITY_ID 在 foo 和 bar 中的定义完全不同,可以按文件名算一个唯一 ID 值出来,互不冲突,也就是实现下面的合并效果:

    #define MY_UNITY_ID <hash(foo.cpp)>
    #include "foo.c"
    #undef MY_UNITY_ID
    #define MY_UNITY_ID <hash(bar.cpp)>
    #include "bar.c"
    #undef MY_UNITY_ID
    

    这看上去似乎很麻烦,但是用户不需要关心这些,xmake 会在合并时候自动处理它们,用户只需要指定这个 Unique ID 的名字就行了,例如下面这样:

    target("test")
        set_kind("binary")
        add_includedirs("src")
        add_rules("c++.unity_build", {batchsize = 2, uniqueid = "MY_UNITY_ID"})
        add_files("src/*.c", "src/*.cpp")
    

    处理全局变量,还有全局的重名宏定义,函数什么的,都可以采用这种方式来避免冲突。

    C++20 Modules

    xmake 采用 .mpp 作为默认的模块扩展名,但是也同时支持 .ixx, .cppm, .mxx 等扩展名。

    早期,xmake 试验性支持过 C++ Modules TS ,但是那个时候,gcc 还不能很好的支持,并且模块间的依赖也不支持。

    最近,我们对 xmake 做了大量改进,已经完整支持 gcc-11/clang/msvc 的 C++20 Modules 构建支持,并且能够自动分析模块间的依赖关系,实现最大化并行编译。

    同时,对新版本的 clang/msvc 也做了更好地处理。

    set_languages("c++20")
    target("test")
        set_kind("binary")
        add_files("src/*.cpp", "src/*.mpp")
    

    更多例子见:C++ Modules

    Lua5.4 运行时支持

    上个版本,我们增加了对 Lua5.3 运行时支持,而在这个版本中,我们进一步升级 Lua 运行时到 5.4 ,相比 5.3 ,运行性能和内存利用率上都有很大的提升。

    不过,目前 xmake 的默认运行时还是 luajit ,预计 2.6.1 版本(也就是下个版本),会正式切到 Lua5.4 作为默认的运行时。

    尽管切换了 Lua 运行时,但是对于用户端,完全是无感知的,并且完全兼容现有工程配置,因为 xmake 原本就对暴露的 api 提供了一层封装, 对于 lua 版本之间存在兼容性问题的接口,例如 setfenv, ffi 等都隐藏在内部,原本就没有暴露给用户使用。

    Keil MDK 工具链支持

    我们在这个版本中,还新增了 Keil/MDK 嵌入式编译工具链的支持,相关例子工程:Example

    xmake 会自动探测 Keil/MDK 安装的编译器,相关 issues #1753

    使用 armcc 编译

    $ xmake f -p cross -a cortex-m3 --toolchain=armcc -c
    $ xmake
    

    使用 armclang 编译

    $ xmake f -p cross -a cortex-m3 --toolchain=armclang -c
    $ xmake
    

    控制台程序

    target("hello")
        add_deps("foo")
        add_rules("mdk.console")
        add_files("src/*.c", "src/*.s")
        add_defines("__EVAL", "__MICROLIB")
        add_includedirs("src/lib/cmsis")
    

    静态库程序

    add_rules("mode.debug", "mode.release")
    
    target("foo")
        add_rules("mdk.static")
        add_files("src/foo/*.c")
    

    Wasi 工具链支持

    之前我们支持了 wasm 平台的 emcc 工具链来构建 wasm 程序,而这里,我们新加了另外一个启用了 WASI 的 Wasm 工具链来替换 emcc 。

    $ xmake f -p wasm --toolchain=wasi
    $ xmake
    

    Circle 工具链支持

    我们还新增了 circle 编译器的支持,这是个新的 C++20 编译器,额外附带了一些有趣的编译期元编程特性,有兴趣的同学可以到官网查看: https://www.circle-lang.org/

    $ xmake f --toolchain=circle
    $ xmake
    

    gcc-8/9/10/11 特定版本支持

    如果用户额外安装了 gcc-11, gcc-10 等特定版本的 gcc 工具链,在本地的 gcc 程序命名可能是 /usr/bin/gcc-11

    一种办法是通过 xmake f --cc=gcc-11 --cxx=gcc-11 --ld=g++-11 挨个指定配置来切换,但非常繁琐。

    所以,xmake 也提供了更加快捷的切换方式:

    $ xmake f --toolchain=gcc-11 -c
    $ xmake
    

    只需要指定 gcc-11 对应的版本名,就可以快速切换整个 gcc 工具链。

    C++17/20 编译器特性检测

    xmake 提供了 check_features 辅助接口来检测编译器特性。

    includes("check_features.lua")
    
    target("test")
        set_kind("binary")
        add_files("*.c")
        add_configfiles("config.h.in")
        configvar_check_features("HAS_CONSTEXPR", "cxx_constexpr")
        configvar_check_features("HAS_CONSEXPR_AND_STATIC_ASSERT", {"cxx_constexpr", "c_static_assert"}, {languages = "c++11"})
    

    config.h.in

    ${define HAS_CONSTEXPR}
    ${define HAS_CONSEXPR_AND_STATIC_ASSERT}
    

    config.h

    /* #undef HAS_CONSTEXPR */
    #define HAS_CONSEXPR_AND_STATIC_ASSERT 1
    

    而在 2.5.9 版本中,我们新增了 c++17 特性检测:

    特性名
    cxx_aggregate_bases
    cxx_aligned_new
    cxx_capture_star_this
    cxx_constexpr
    cxx_deduction_guides
    cxx_enumerator_attributes
    cxx_fold_expressions
    cxx_guaranteed_copy_elision
    cxx_hex_float
    cxx_if_constexpr
    cxx_inheriting_constructors
    cxx_inline_variables
    cxx_namespace_attributes
    cxx_noexcept_function_type
    cxx_nontype_template_args
    cxx_nontype_template_parameter_auto
    cxx_range_based_for
    cxx_static_assert
    cxx_structured_bindings
    cxx_template_template_args
    cxx_variadic_using

    还新增了 c++20 特性检测:

    特性名
    cxx_aggregate_paren_init
    cxx_char8_t
    cxx_concepts
    cxx_conditional_explicit
    cxx_consteval
    cxx_constexpr
    cxx_constexpr_dynamic_alloc
    cxx_constexpr_in_decltype
    cxx_constinit
    cxx_deduction_guides
    cxx_designated_initializers
    cxx_generic_lambdas
    cxx_impl_coroutine
    cxx_impl_destroying_delete
    cxx_impl_three_way_comparison
    cxx_init_captures
    cxx_modules
    cxx_nontype_template_args
    cxx_using_enum

    Xrepo 包虚拟环境管理

    进入虚拟环境

    xmake 自带的 xrepo 包管理工具,现在已经可以很好的支持包虚拟机环境管理,类似 nixos 的 nixpkgs 。

    我们可以通过在当前目录下,添加 xmake.lua 文件,定制化一些包配置,然后进入特定的包虚拟环境。

    add_requires("zlib 1.2.11")
    add_requires("python 3.x", "luajit")
    
    $ xrepo env shell
    > python --version
    > luajit --version
    

    我们也可以在 xmake.lua 配置加载对应的工具链环境,比如加载 vs 的编译环境。

    set_toolchains("msvc")
    

    管理虚拟环境

    我们可以使用下面的命令,把指定的虚拟环境配置全局注册到系统中,方便快速切换。

    $ xrepo env --add /tmp/base.lua
    

    这个时候,我们就保存了一个名叫 base 的全局虚拟环境,我们可以通过 list 命令去查看它。

    $ xrepo env --list
    /Users/ruki/.xmake/envs:
      - base
    envs(1) found!
    

    我们也可以删除它。

    $ xrepo env --remove base
    

    切换全局虚拟环境

    如果我们注册了多个虚拟环境,我们也可以快速切换它们。

    $ xrepo env -b base shell
    > python --version
    

    或者直接加载指定虚拟环境运行特定命令

    $ xrepo env -b base python --version
    

    xrepo env -b/--bind 就是绑定指定的虚拟环境,更多详情见:#1762

    Header Only 目标类型

    对于 target ,我们新增了 headeronly 目标类型,这个类型的目标程序,我们不会实际编译它们,因为它没有源文件需要被编译。

    但是它包含了头文件列表,这通常用于 headeronly 库项目的安装,IDE 工程的文件列表生成,以及安装阶段的 cmake/pkgconfig 导入文件的生成。

    例如:

    add_rules("mode.release", "mode.debug")
    
    target("foo")
        set_kind("headeronly")
        add_headerfiles("src/foo.h")
        add_rules("utils.install.cmake_importfiles")
        add_rules("utils.install.pkgconfig_importfiles")
    

    更多详情见:#1747

    从 CMake 中查找包

    现在 cmake 已经是事实上的标准,所以 CMake 提供的 find_package 已经可以查找大量的库和模块,我们完全复用 cmake 的这部分生态来扩充 xmake 对包的集成。

    我们可以通过 find_package("cmake::xxx") 去借助 cmake 来找一些包,xmake 会自动生成一个 cmake 脚本来调用 cmake 的 find_package 去查找一些包,获取里面包信息。

    例如:

    $ xmake l find_package cmake::ZLIB
    {
      links = {
        "z"
      },
      includedirs = {
        "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
    15.sdk/usr/include"
      },
      linkdirs = {
        "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
    15.sdk/usr/lib"
      }
    }
    $ xmake l find_package cmake::LibXml2
    {
      links = {
        "xml2"
      },
      includedirs = {
        "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/libxml2"
      },
      linkdirs = {
        "/usr/lib"
      }
    }
    

    指定版本

    find_package("cmake::OpenCV", {required_version = "4.1.1"})
    

    指定组件

    find_package("cmake::Boost", {components = {"regex", "system"}})
    

    预设开关

    find_package("cmake::Boost", {components = {"regex", "system"}, presets = {Boost_USE_STATIC_LIB = true}})
    set(Boost_USE_STATIC_LIB ON) -- will be used in FindBoost.cmake
    find_package(Boost REQUIRED COMPONENTS regex system)
    

    设置环境变量

    find_package("cmake::OpenCV", {envs = {CMAKE_PREFIX_PATH = "xxx"}})
    

    指定自定义 FindFoo.cmake 模块脚本目录

    mydir/cmake_modules/FindFoo.cmake

    find_package("cmake::Foo", {moduledirs = "mydir/cmake_modules"})
    

    包依赖集成

    package("xxx")
        on_fetch(function (package, opt)
             return package:find_package("cmake::xxx", opt)
        end)
    package_end()
    
    add_requires("xxx")
    

    包依赖集成(可选组件)

    package("boost")
        add_configs("regex",   { description = "Enable regex.", default = false, type = "boolean"})
        on_fetch(function (package, opt)
             opt.components = {}
             if package:config("regex") then
                 table.insert(opt.components, "regex")
             end
             return package:find_package("cmake::Boost", opt)
        end)
    package_end()
    
    add_requires("boost", {configs = {regex = true}})
    

    相关 issues: #1632

    添加自定义命令到 CMakelists.txt

    我们进一步改进了 cmake 生成器,现在可以将 rule 里面自定义的脚本序列化成命令列表,一起生成到 CMakelists.txt

    不过目前只能支持 batchcmds 系列脚本的序列化。

    rule("foo")
        after_buildcmd(function (target, batchcmds, opt)
            batchcmds:show("hello xmake!")
            batchcmds:cp("xmake.lua", "/tmp/")
            -- batchcmds:execv("echo", {"hello", "world!"})
            -- batchcmds:runv("echo", {"hello", "world!"})
        end)
    
    target("test")
        set_kind("binary")
        add_rules("foo")
        add_files("src/*.c")
    

    它将会生成类似如下的 CMakelists.txt

    # ...
    add_custom_command(TARGET test
        POST_BUILD
        COMMAND echo hello xmake!
        VERBATIM
    )
    add_custom_command(TARGET test
        POST_BUILD
        COMMAND cp xmake.lua /tmp/
        VERBATIM
    )
    target_sources(test PRIVATE
        src/main.c
    )
    

    不过 cmake 的 ADD_CUSTOM_COMMAND PRE_BUILD 实际效果在不同生成器上,差异比较大,无法满足我们的需求,因此我们做了很多处理来支持它。

    相关 issues: #1735

    改进对 NixOS 的安装支持

    我们还改进了 get.sh 安装脚本,来更好地支持 nixOS 。

    更新内容

    新特性

    • #1736: 支持 wasi-sdk 工具链
    • 支持 Lua 5.4 运行时
    • 添加 gcc-8, gcc-9, gcc-10, gcc-11 工具链
    • #1623: 支持 find_package 从 cmake 查找包
    • #1747: 添加 set_kind("headeronly") 更好的处理 headeronly 库的安装
    • #1019: 支持 Unity build
    • #1438: 增加 xmake l cli.amalgamate 命令支持代码合并
    • #1765: 支持 nim 语言
    • #1762: 为 xrepo env 管理和切换指定的环境配置
    • #1767: 支持 Circle 编译器
    • #1753: 支持 Keil/MDK 的 armcc/armclang 工具链
    • #1774: 添加 table.contains api
    • #1735: 添加自定义命令到 cmake 生成器
    • #1781: 改进 get.sh 安装脚本支持 nixos

    改进

    • #1528: 检测 c++17/20 特性
    • #1729: 改进 C++20 modules 对 clang/gcc/msvc 的支持,支持模块间依赖编译和并行优化
    • #1779: 改进 ml.exe/x86 ,移除内置的 -Gd 选项
    3 条回复    2021-11-02 00:38:51 +08:00
    ly841000
        1
    ly841000  
       2021-11-01 09:23:55 +08:00
    lua 作为配置语言,确实比 make/CMake 之类的自定义 DSL 好用太多了
    waruqi
        2
    waruqi  
    OP
       2021-11-01 09:45:19 +08:00 via Android
    @ly841000 是的,而且学习成本低,并且轻量,xmake 内置 lua 完全无任何依赖
    kilasuelika
        3
    kilasuelika  
       2021-11-02 00:38:51 +08:00 via Android
    不错不错,越来越完善了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3723 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 00:52 · PVG 08:52 · LAX 16:52 · JFK 19:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.