• HOME
  • JOIN
  • RSS
  • Friday, September 16, 2022

    使用 pkg-config

    pkg-config 是一个在源代码编译时查询已安装的库的使用接口的计算机工具软件。pkg-config原本是设计用于Linux的,但现在在各个版本的BSD、windows、Mac OS X和Solaris上都有着可用的版本。

    它输出已安装的库的相关信息,包括:

    • C/C++编译器需要的输入参数
    • 链接器需要的输入参数
    • 已安装软件包的版本信息

    使用 cmake 生成 .pc 文件

    一个比较规范的 foo.pc.in 如下:

    prefix=@CMAKE_INSTALL_PREFIX@
    exec_prefix=${prefix}
    libdir=@CMAKE_INSTALL_FULL_LIBDIR@
    includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@/libimageviewer
    
    Name: foo
    Description: foo is a demo lib
    Version: @PROJECT_VERSION@
    Libs: -L${libdir} -lfoo
    Cflags: -I${includedir}
    Requires: Qt5Core Qt5Gui Qt5Widgets 
    

    这里的 Name 是显示的名称,文件名才是搜索用,显示名一般不要全部大写。

    Requires:设置 foo 的依赖, 这里有2个条件:

    1. 必须是传播的构建依赖,即某软件构建依赖 foo,构建环境必须有 bar 才可以编译
    2. 必须是提供 .pc 文件的依赖

    另外不要忘记加 -L 参数(链接库地址),-I 参数(头文件地址)。

    相关修改: https://github.com/linuxdeepin/image-editor/pull/27

    其中路径问题(libdir/includedir)这里再强调一下, 不规范做法:

    prefix=/usr
    exec_prefix=${prefix}
    libdir=${prefix}/lib
    includedir=${prefix}/include/foo
    

    这种是硬编码路径,可移植性非常差。

    prefix=@CMAKE_INSTALL_PREFIX@
    exec_prefix=${prefix}
    libdir=${prefix}/lib
    includedir=${prefix}/include/foo
    

    相比上种方法,prefix 使用 cmake 变量,可以应对 prefix 不是 /usr 的情况了。

    prefix=@CMAKE_INSTALL_PREFIX@
    exec_prefix=${prefix}
    libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@
    includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@/foo
    

    相比上种方法,lib/include 目录使用 GNUInstallDirs 变量代替,灵活度更高。

    但是,这种并非最好的,因为(1)GNUInstallDirs 有特殊的拼接规则,如 PREFIX=/,LIBDIR=lib,应该拼接成 /usr/include 而非 /include。(2)CMAKE_INSTALL_LIBDIR 虽然默认是相对路径,但允许被直接设置成绝对路径。

    写在 .pc.in 只能简单拼接,无法判断特殊情况,最好由 cmake 进行拼接。GNUInstallDirs 提供 FULL 版本变量(本文第一个示例)就是智能拼接出来的,建议使用。当然,基于此自行设置变量也可以: set (INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_FULL_INCLUDEDIR}/libdtk-${CMAKE_PROJECT_VERSION}/DCore")

    使用 qmake 生成 pkgcconfig 文件

    CONFIG += create_prl create_pc
    # 生成 .pc 文件需要 .prl 中间文件,如果不需要可以使用 no_install_prl
    
    QMAKE_PKGCONFIG_NAME = foo
    QMAKE_PKGCONFIG_DESCRIPTION = foo is demo
    QMAKE_PKGCONFIG_INCDIR = $${HEADERDIR}
    QMAKE_PKGCONFIG_LIBDIR = $${LIBDIR}
    QMAKE_PKGCONFIG_DESTDIR = pkgconfig
    

    其中的 HEADERDIR,LIBDIR 是需要在 .pro 文件中设置的安装路径。

    cmake 使用 pkg-config

    不要混淆 find_package 和 pkg_check_modules

    cmake 导入

    find_package(PkgConfig REQUIRED) # 导入此模块才可以使用 pkg_check_modules
    pkg_check_modules(DtkCore REQUIRED dtkcore)
    message("${DtkCore_VERSION}")
    message("${DtkCore_LIBRARIES}") # 对应 -l 参数
    message("${DtkCore_LIBRARY_DIRS}") # 对应 -L 参数
    message("${DtkCore_INCLUDE_DIRS}") # 对应 -I 参数
    
    pkg_check_modules(Dtk REQUIRED dtkcore dtkgui dtkwidget)
    message("${Dtk_LIBRARIES}")
    message("${Dtk_LIBRARY_DIRS}")
    message("${Dtk_INCLUDE_DIRS}")
    

    参考资料

    qmake 使用 pkg-config

    CONFIG += link_pkgconfig
    PKGCONFIG += foo
    

    CMake 中有许多关于编译器和链接器的设置。当你需要添加一些特殊的需求,你应该首先检查 CMake 是否支持这个需求,如果支持的话,你就可以不用关心编译器的版本,一切交给 CMake 来做即可。 更棒的是,你可以在 CMakeLists.txt 表明你的意图,而不是通过开启一系列标志 (flag) 。

    优化参数(-O0/-O1/-O2/-O3/-Ofast/-Os) 和 debug 参数 (-g)

    CMAKE_BUILD_TYPE 包括 Debug, Release, RelWithDebInfoMinSizeRe 4 个选项

    它们对应参数是(不同编译平台可能有少许区别):

    1. Release: -O3 -DNDEBUG
    2. Debug: -O0 -g
    3. RelWithDebInfo: -O2 -g -DNDEBUG
    4. MinSizeRel: -Os -DNDEBUG

    这些参数可以通过 CMAKE_CXX_FLAGS_DEBUG,CMAKE_CXX_FLAGS_RELEASE… 查看,并不直接体现在 CMAKE_CXX_FLAGS 中。

    CMAKE_BUILD_TYPE 的默认值不是Release,也不属于上面4种,而是空值,即不附加任何参数。

    全局(或非debug模式)设置 -O3:

    建议改为将 CMAKE_BUILD_TYPE 设置默认 Release

    Release 模式加 O3, Debug 模式加 -g 等等

    不用加,这些都是无用功

    Release 模式加 -g

    如果软件发行版本需要 debug 符号,请用 RelWithDebInfo 模式

    Debug 加 -O3

    高级别的优化会严重影响 debug 调试, 可以改用 -Og,它允许不影响调试的优化

    同时有 -O1 -O3

    -O0/-O1/-O2/-O3/-Ofast 开启的优化参数前依次递增,是完全的子集关系,不是多多益善的。 -Os 是在 -O2 的基础上,尽可能优化程序大小。

    Release 模式使用 -Ofast 或其他优化级别。

    最好是 string(REPLACE "-O3" "-Ofast" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}),这样就没有原来的-O3了 如果是是降低优化基本,则必须这样替换。

    参考资料:

    de

    参考修改:

    c++标准版本 (比如 -std=c++11)

    直接使用 set(CMAKE_CXX_STANDARD 11)

    部分项目设置了 CMAKE_CXX_STANDARD 为 14/17, 又加了参数 -std=c++11,这是冲突的,请检查。 比如: https://github.com/linuxdeepin/deepin-movie-reborn/blob/0a08e29e78c4f35f787b999d857f696f804f8641/CMakeLists.txt#L18

    Hardening

    DEB_BUILD_HARDENING_FORMAT (gcc/g++ -Wformat -Wformat-security -Werror=format-security) DEB_BUILD_HARDENING_FORTIFY (gcc/g++ -D_FORTIFY_SOURCE=2) DEB_BUILD_HARDENING_STACKPROTECTOR (gcc/g++ -fstack-protector-strong) DEB_BUILD_HARDENING_PIE (gcc/g++ -fPIE -pie) DEB_BUILD_HARDENING_RELRO (ld -z relro) DEB_BUILD_HARDENING_BINDNOW (ld -z now)

    as-need 参数

    已知 as-need 参数 break 了 mold 连接器,可用 as-needed 代替。 相关:https://github.com/linuxdeepin/developer-center/issues/3345

    dl 库

    如果你需要链接到 dl 库,在 Linux 上可以使用 -ldl 标志,不过在 CMake 中只需要在 target_link_libraries 命令中使用内置的 CMake 变量 ${CMAKE_DL_LIBS} 。这里不需要模组或者使用 find_package 来寻找它。(这个命令包含了调用 dlopendlclose 的一切依赖)

    程序间优化(Interprocedural optimization)

    INTERPROCEDURAL_OPTIMIZATION,最有名的是 链接时间优化 以及 -flto 标志,这在最新的 CMake 版本中可用。你可以通过变量 CMAKE_INTERPROCEDURAL_OPTIMIZATION( CMake 3.9+ 可用)或对目标指定 INTERPROCEDURAL_OPTIMIZATION 属性来打开它。在 CMake 3.8 中添加了对 GCC 及 Clang 的支持。如果你设置了 cmake_minimum_required(VERSION 3.9) 或者更高的版本(参考 CMP0069),当在编译器不支持 INTERPROCEDURAL_OPTIMIZATION 时,通过变量或属性启用该优化会产生报错。你可以使用内置模块 CheckIPOSupported 中的 check_ipo_supported() 来检查编译器是否支持 IPO 。下面是基于 CMake 3.9 的一个例子:

    include(CheckIPOSupported)
    check_ipo_supported(RESULT result)
    if(result)
      set_target_properties(foo PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
    endif()
    

    位置无关的代码

    在 g++ 中,对编译生成动态库是 -fPIC/-fpic,对生成可执行文件是 -fPIE/-fpie。

    如果要把静态库链接到动态库,这一选项是必须的。其他情况也建议开启,可以防范一些内存攻击。

    在 cmake 中,对于动态库(SHARED and MODULE library),这一选项是默认开启的。对于静态库,在 cmake 3.14 版本之后(CMP0083 新标准),默认值是 POSITION_INDEPENDENT_CODE,而该值默认为假。

    可以通过 set(POSITION_INDEPENDENT_CODE True) 让静态库默认增加 “-fpic” 编译。

    最好通过 set_property 设置此选项:set_property(TARGET foo PROPERTY POSITION_INDEPENDENT_CODE TRUE),foo 为对应静态库或者可执行程序。

    此外,推荐使用 check_pie_supported(),检查一下编译器是否支持。

    参考资料:

    -lpthread

    POSIX thread 是基于 C/C++ 的标准线程库。在 cmake 3.1 以上的版本提供了 FindThreads 模块, 如果设置 THREADS_PREFER_PTHREAD_FLAG 变量,会优先使用 pthread。

    set(THREADS_PREFER_PTHREAD_FLAG ON)
    find_package(Threads REQUIRED)
    target_link_libraries(my_app PRIVATE Threads::Threads)
    

    参考文档:

    https://modern-cmake-cn.github.io/Modern-CMake-zh_CN/chapters/features/small.html

    设置时不要覆盖之前的选项

    错误示例:set(CMAKE_CXX_FLAGS “-Wall”) 正确示例:set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -Wall”)

    如果确实有覆盖的要求,请在源码注释说明,因为一般出现覆盖 flag 很可能是失误。

    判断架构

    遗留代码有使用 dpkg-architecture 判断的,改用 CMAKE_SYSTEM_PROCESSOR。

    dpkg-architecture 是 debian 系特有的,并不通用。 除了 deb 安装器等不考虑像其他发行版移植的应用,不要在 CMakeList 里使用 dpkg 系列命令。

    CMAKE_L_FLAGS

    部分项目 使用了 CMAKE_L_FLAGS,这不是 cmake 标准的变量,这是想用 CMAKE_EXE_LINKER_FLAGS ?

    glib-compile-schemas

    gsettings schemas 安装后需要编译一下(一般目录是 /usr/share/glib-2.0/schemas)才能使用,这个应该是安装后进行的,用对应目录所有 gschema.xml 再编译出一个 gschemas.compiled 文件。

    像下面这样在 CMakeList.txt 编译是不需要的: install(CODE "execute_process(COMMAND glib-compile-schemas ${CMAKE_INSTALL_PREFIX}/share/glib-2.0/schemas)") 相关修改

    可执行程序

    判断某个可执行程序是否存在

    不推荐做法: 根据硬编码路径判断某个文件是否存在。比如判断 QFile().exists(/usr/bin/Foo)

    推荐做法:根据 PATH 寻找可执行文件, 一般不需要自行读取 PATH,比如 QT 可以使用:

    Exist = !QStandardPaths::findExecutable("Foo").isEmpty();
    

    比如 glib 可以使用 find_program_in_path。

    修改示例:

    执行某个可执行程序

    同样,推荐用 PATH 寻找,尽量不使用绝对路径。

    Qt 的 QProcess 会自动处理 PATH 环境变量,因此 QProcess::execute("/usr/bin/touch", QStringList() << sessionCacheFile) 可以直接改成 QProcess::execute("touch", QStringList() << sessionCacheFile)

    修改示例:

    动态库路径

    尽量不要硬编码 /urs/lib

    qt 应用可以使用 QLibraryInfo::location(QLibraryInfo::LibrariesPath)

    头文件

    绝对不要有 #include </usr/include/xxx.h> 这种代码,c/c++ 头文件会自动在 /usr/include 里寻找,直接使用 #include <xxx.h>

    此外,如果库提供了 pkg-config 文件,提供的 -I 参数会指定头文件位置,如果提供 Config.cmake (cmake用)文件,一般会提供 Foo_INCLUDE_DIR 变量,如果提供 .pri (qmake 用)文件,由 QT.foo.includes 提供头文件位置。xxx.h 是相对提供的路径寻找。

    对于没有提供任何开发文件的库,也可以使用 cmake 提供的 CheckIncludeFile 模块寻找头文件。

    应用数据(/use/share)

    /usr/share,一般保存与架构无关的只读数据。此目录同样不应该硬编码,在 XDG 规范中,这种数据的目录使用环境变量 XDG_DATA_DIRS 设置,如果读不到对应环境变量再去读取 /usr/local/share:/usr/share。

    使用 qt 的应用该类型硬编码路径应该使用 QStandardPaths 或者 libqtxdg 代替。

    参考:XDG Base Directory

    在此目录寻找某文件,推荐使用 QStandardPaths::standardLocations 参考修改:

    /usr/share/applications

    直接使用 QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)

    相关修改:

    /usr/share/AppName

    读取本应用数据,qt 软件可以使用 QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation)

    相关修改:

    导入翻译

    部分应用导入翻译使用了 /usr/share 示例, 应该改为使用 QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation), 此外使用了 dtk 的应用是直接用 DApplication 的 loadTranslator 导入翻译。

    Shebangs

    #!/bin/bash -> #!/usr/bin/env bash
    #!/usr/bin/python -> #!/usr/bin/env python
    

    并非所有系统都有 /bin 目录和 /usr/bin 目录,比如 NixOS 和 GNU Guix 操作系统。 env 可以根据环境变量 $PATH 寻找可执行程序(如 bash),比硬编码路径更加灵活,可以提高可移植性。

    不过并非所有脚本都适合使用 env, 有 2 种情况:

    • 对安全性要求高的脚本(如以 root 权限运行),硬编码可以避免通过相关 PATH 让 bash 指向恶意程序。
    • 带有参数的 Shebangs 头,如 #!/usr/bin/perl -w 不能改成 #!/usr/bin/env perl -w

    对除此之外的大部分的 Shebangs 脚本来说,使用 env 是更好的方案。

    参考资料:

    相关 issues:

    使用 GNUInstallDirs.cmake模块

    在指定安装路径时,应当使用变量而非写死安装目录,以便于在不完全符合 FHS 的系统上安装,提高程序的可移植性。对于使用何种变量, GUN 提出了适用于 unix 系统的 GNU标准安装目录,GNU/Linux 上使用的就是这套标准的变体。cmake 官方提供了 GNUInstallDirs 模块,定义了一组标准的变量,用于安装不同类型文件到规范指定的目录中。

    要使用这个模块,在 CMakeLists.txt 添加一行 include(GNUInstallDirs) 即可导入。如果你发现 CMAKE_INSTALL_XXXX 的值为空,大概率是缺少这一行。注意导入模块需要放在使用变量之前。

    前缀值 CMAKE_INSTALL_PREFIX

    CMAKE_INSTALL_PREFIX(后面简称 PREFIX) 是一个非常特殊的变量,在 CMakeLists.txt 中所有的相对路径都会默认把 PREFIX 作为前缀进行拼接,组成绝对路径。这一变量是 cmake 基础变量,不导入 GNUInstallDirs 模块也会存在。

    举一个例子,假如之前要把文件安装进 /usr/share 目录

    install(FILES ${QM_FILES} DESTINATION  /usr/share/deepin-calculator/translations)
    

    如果设置了 PREFIX=/usr,只需要改成相对路径 share,cmake 会自动拼接成 /usr/share

    install(FILES ${QM_FILES} DESTINATION  share/deepin-calculator/translations)
    

    这样写,如果安装前缀改变,比如改成 /usr/local,只需要修改 CMAKE_INSTALL_PREFIX,而不用改 CMakeLists.txt 源码。

    目前 CMAKE_INSTALL_PREFIX 按 GNU 的标准默认值是 /usr/local。而在 dde 的项目中,一般期望前缀值是 /usr。我们当然可以通过 cmake -DCMAKE_INSTALL_PREFIX=/usr 来修改 PREFIX,但最好在项目中添加下面 3 行修改一下 PREFIX 的默认值:

    if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
        set(CMAKE_INSTALL_PREFIX /usr)
    endif ()
    

    这里容易犯两种错误:

    1. 没有检查 CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT 就直接 set:这会导致用户传入的参数失效,相当于硬编码了路径。
    2. 使用 if (NOT DEFINE CMAKE_INSTALL_PREFIX ) 判断用户是否传参了 PREFIX:实际上 PREFIX 无论什么情况都是有定义的, 只能使用 CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT 判断是否是默认值。

    对 CMAKE_INSTALL_PREFIX 的配置会对子目录生效,在 cmake 3.14 之前,先应用安装规则再处理 add_subdirectory,而 3.14 及后续版本,会按照声明的顺序运行所有安装规则/add_subdirectory,因此,除非你想给子目录单独配置,上述的设置应写在 add_subdirectory 的前面。 见 CMP0082

    GNUInstallDirs 变量

    在 GNUInstallDirs 中,定义了一些 GNU 标准安装目录的变量,提供给定类型文件的安装路径。这些值可以传递给对应 install() 命令的 DESTINATION 选项。它们通常是相对于安装前缀(PREFIX)的相对路径,以便于以可重定位的方式将其拼接为绝对路径。当然,它们也允许赋值为绝对路径。

    CMAKE_INSTALL_XXXX

    • CMAKE_INSTALL_BINDIR 用户可执行程序( bin

    • CMAKE_INSTALL_SBINDIR 系统管理员可执行程序( sbin

    • CMAKE_INSTALL_LIBEXECDIR 可执行库文件( libexec

    • CMAKE_INSTALL_SYSCONFDIR 单机只读数据/read-only single-machine data( etc

    • CMAKE_INSTALL_SHAREDSTATEDIR 架构无关的可修改数据/modifiable architecture-independent data ( com

    • CMAKE_INSTALL_LOCALSTATEDIR 单机可修改数据/modifiable single-machine data( var

    • CMAKE_INSTALL_RUNSTATEDIR 3.9版中加入:运行时可修改数据( LOCALSTATEDIR/run

    • CMAKE_INSTALL_LIBDIR 目标代码库/object code libraries ( liblib64 ) (在 Debian 上当 PREFIX 是 /usr 时,LIBDIR 也可能是 lib/<multiarch-tuple>

    • CMAKE_INSTALL_INCLUDEDIR C语言头文件( include

    • CMAKE_INSTALL_OLDINCLUDEDIR non-gcc 的C语言头文件( /usr/include

    • CMAKE_INSTALL_DATAROOTDIR 与架构无关的只读数据根目录( share

    • CMAKE_INSTALL_DATADIR 与架构无关的只读数据( DATAROOTDIR

    • CMAKE_INSTALL_INFODIR info 文档( DATAROOTDIR/info

    • CMAKE_INSTALL_LOCALEDIR 与语言相关的数据/locale-dependent data( DATAROOTDIR/locale

    • CMAKE_INSTALL_MANDIR man 文档( DATAROOTDIR/man

    • CMAKE_INSTALL_DOCDIR 文档根目录( DATAROOTDIR/doc/PROJECT_NAME

    需要注意的是 DATAROOTDIR 是 DATADIR,LOCALEDIR,MANDIR 和 DOCDIR 共同前缀,不应该在 install 中直接使用 DATAROOTDIR 作为参数,而是应该使用 DATADIR 代替 。类似 /usr/share/man 的路径应该用 MANDIR 代替,而不是 DATADIR/man

    CMAKE_INSTALL_FULL_XXXX

    CMAKE_INSTALL_BINDIR 推荐用相对路径,但也可以使用绝对路径,有些时候我们需要绝对路径(比如生成 pkgconfig 文件时),直接加 PREFIX 拼接并不好(因为 BINDIR 可能已经是绝对路径了),这时,我们可以直接使用 CMAKE_INSTALL_FULL_BINDIR,这是由 GNUInstallDirs 模块提供,自动从 BINDIR 计算出的绝对路径。如果 BINDIR 是绝对路径,直接相等。如果 BINDIR 是相对路径,则等于 BINDIR 按照一定规则与 PREFIX 拼接而成的值。

    用例: https://github.com/linuxdeepin/dde-dock/pull/556

    当然,install 是没有必要用 CMAKE_INSTALL_FULL_XXXX 的(当然用也可以),因为它会自动判断,并拼接相对路径。

    前缀拼接的特殊情况

    GNUInstallDirs 前缀拼接存在一些特殊情况需要注意:

    1. CMAKE_INSTALL_PREFIX=/

    除了 SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 正常拼接外, 其他值都会增加一个 usr/ 前缀 。比如 INCLUDEDIR 默认值 include ,变成 usr/include, 最终拼接成 /usr/include。

    这里自动增加 usr 是符合 GNU 目录标准的,因为这些路径是符号链接。
    ~ ❯❯❯ readlink /lib
    usr/lib
    ~ ❯❯❯ readlink /bin
    usr/bin
    

    2. CMAKE_INSTALL_PREFIX=/usr

    SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 拼接时只拼接 “/”。 比如, SYSCONFDIR 默认值 etc 会拼接成 /etc。

    3. CMAKE_INSTALL_PREFIX=/opt/…

    SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 会向后拼接。 比如, SYSCONFDIR 会变成 /etc/opt/….

    参考文档

    Sunday, August 14, 2022

    DDE 控制中心自启动管理插件可单独编译、集成至 DDE 控制中心、集中展示并管理Deepin系统中软件开机自启动设置。

    参赛信息

    本项目参加2023全国大学生计算机系统能力大赛操作系统设计赛-功能挑战赛,选题为proj223-control-center-startup-management-plugin

    参赛小组:

    • 复旦大学 朱元依
    • 复旦大学 沈扬
    • 复旦大学 朱俊杰

    指导老师:

    • 复旦大学 张亮
    • 复旦大学 陈辰
    • 企业导师 王子冲

    完成情况

    1、DDE 控制中心自启动管理插件

    任务完成情况
    (必须)完成一个控制中心插件,能够展示当前所有开机启动项的列表完成
    (必须)插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目完成
    (必须)能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项完成
    (必须)编写博客,记录开发过程的心得与体会,并将博客投递至 planet.deepin.org完成博客编写,待deepin官方审核

    成果展示

    功能展示视频链接:

    链接:https://pan.baidu.com/s/1YgvhbZ8pypE4CFhuBYw3eg 提取码:rwcc

    插件图标:

    前端页面

    自启动管理窗口:

    自启动管理页面

    添加自启动应用选择界面:

    应用选择页面

    部署方式

    1、系统环境

    开发环境:Deepin V23Beta版

    系统架构:x86

    镜像下载链接:https://mirrors.ustc.edu.cn/deepin-cd/releases/23-Beta/

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:https://blog.csdn.net/qq_44133136/article/details/105887560

    2、配置 Deepin 插件环境

    安装依赖包:

    sudo apt build-dep .
    sudo apt install -y qt5-default
    sudo apt-get install dde-control-center-dev
    

    3、安装插件

    sudo sh install.sh
    

    安装成功后,打开控制中心,会看到以下自启动插件图标,即为安装成功:

    控制中心插件图标

    此时,如果进入/usr/lib/x86_64-linux-gnu/dde-control-center/modules/文件夹,看到编译出的.so文件已经被下载到该文件夹中:

    插件安装位置文件夹

    4、卸载插件

    sudo sh uninstall.sh
    

    重启控制中心,可以看到原本的“自启动管理”图标消失,即为卸载成功。

    分工

    朱元依:插件前端开发

    沈扬:环境配置,编写配置文件

    朱俊杰:后端接口开发(添加、删除、启用、禁用)

    开发计划

    第一步(6/26~7/8)

    • 调研DDE Control Center框架等相关内容
    • 设计前端界面
    • 分工

    第二步(7/9~7/15)

    • 编译教程中的Hello World控制中心插件
    • 设计插件架构

    第三步(7/16~7/22)

    • 配置环境,编译V20示例插件
    • 设计后端接口
    • 修改windowoperationcategory下的文件

    第四步(7/23~7/29)

    • 配置环境,编译Default-AppSelf Start-up插件
    • Debug

    第五步(7/30~8/10)

    • 修改翻译、文字颜色问题
    • 撰写文档

    Wednesday, August 10, 2022

    什么是REUSE

    REUSE是一个工具,准确的来说是一个帮助我们下载开源许可证书和检查开源声明是否合规的脚本:

    使用REUSE配置项目

    安装

    REUSE依赖于本地的python环境,所以需要首先安装python和pip

    sudo apt install python3
    curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
    sudo python3 get-pip.py
    

    然后安装REUSE

    pip3 install --user reuse
    
    

    在此之后,确保它~/.local/bin在您的$PATH中。在 Windows 上,您的环境所需的路径可能类似于 %USERPROFILE%\AppData\Roaming\Python\Python39\Scripts,具体取决于您安装的 Python 版本。

    使用

    初始化项目

    进入一个项目,使用reuse init命令可以初始化项目,此时在你的命令行菜单上会以交互的方式询问你需要的许可证和版权所有人以及邮箱。注意:许可证的名称需要规范:许可证名称

    如果你在这一步遗漏了也没事,可以使用reuse download <许可证名称>的方式下载所需的许可证,这些许可证会一并放入项目下的License文件夹中

    为代码文件添加版权声明

    使用自动化脚本添加

    含有代码逻辑文件添加版权声明方法为:

    reuse addheader --copyright="UnionTech Software Technology Co., Ltd." --license=GPL-3.0-or-later -r <目录>
    

    这只是一个简单的例子,本质上resuse还有更多的用法:

    --copyright-style可以将默认值更改 SPDX-FileCopyrightText为以下样式之一:

    spdx:           SPDX-FileCopyrightText: <year> <statement>
    spdx-symbol:    SPDX-FileCopyrightText: © <year> <statement>
    string:         Copyright <year> <statement>
    string-c:       Copyright (C) <year> <statement>
    string-symbol:  Copyright © <year> <statement>
    symbol:         © <year> <statement>
    

    reuse不会限制你使用多少个copyright 也不会限制你使用的license的个数。

    reuse 会猜测你使用的语言,并且添加对应的注释格式,如果不能正确添加或者需要更多的需求可以考虑下面的方法。

    手动进行添加

    reuse允许你手动进行添加版权声明,只要你的版权声明符合它的规范即可。你所需要的是:充分的耐心,键盘上未曾损坏的Ctrl C V 三个按键即可完成这一切。我个人建议是用上面提供的自动化脚本添加一个版权声明作为模板,然后自己修改下,就可以展现你作为CV工程师的实力了。

    自动进行添加

    理论上reuse不限制你添加版权声明的方式,你可以选择你喜欢的脚本语言来完成这一切。

    为非代码文件添加版权声明

    在项目中是存在非代码文件,比如图片,音频等等。部分也是受到版权保护的,但是我们也没法直接在上面操作(别和我说水印和音频水印,这俩玩意不能无损添加)所以我们要使用一个外置的dep5文件去声明这些非代码文件的版权,dep5文件在执行项目init之后就已经自动帮你生成好了,所以直接写就是的了。比如:

    # css
    Files: *.css
    Copyright: None
    License: CC0-1.0
    

    第一行就是注释 ,第二行是适合的文件,第三行是版权信息(如果使用cc0,则填写None) ,第四行是使用的licence。第一行的注释你可以选择不写,但是为了以后的可读性,强烈建议你写上,甚至可以作为分类的标签。

    第二行文件,支持使用通配符进行模糊匹配,但是不支持正则表达式:仅支持,*匹配任意多个字符 匹配一个字符。所以可以利用通配符来制定匹配某一个文件夹下面的所有文件

    例子:

    # assets
    Files: styleplugins/dstyleplugin/assets/*
    Copyright: UnionTech Software Technology Co., Ltd.
    License: GPL-3.0-or-later
    

    这个例子展示了有版权信息的情况

    例子:

    # png svg
    Files: platformthemeplugin/icons/* styleplugins/chameleon/menu_shadow.svg styles/images/woodbackground.png
           styles/images/woodbutton.png tests/iconengines/builtinengine/icons/actions/icon_Layout_16px.svg
           tests/iconengines/svgiconengine/icon_window_16px.svg
    Copyright: None
    License: CC0-1.0
    

    这个例子展示了为多个文件路径及文件配置版权信息

    强烈推荐你使用路径和具体文件名的方式来制定版权信息,而要避免大范围使用通配符的情况

    例子:

    Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
    Upstream-Name: qt5integration
    Upstream-Contact: UnionTech Software Technology Co., Ltd.  <>
    Source: https://github.com/linuxdeepin/qt5integration
    
    # README
    Files: README.md CHANGELOG.md
    Copyright: None
    License: CC0-1.0
    
    # assets
    Files: styleplugins/dstyleplugin/assets/*
    Copyright: UnionTech Software Technology Co., Ltd.
    License: GPL-3.0-or-later
    
    # Project file
    Files: *.pro *.prf *.pri
    Copyright: None
    License: CC0-1.0
    
    # css
    Files: *.css
    Copyright: None
    License: CC0-1.0
    
    # qrc
    Files: *.qrc
    Copyright: None
    License: CC0-1.0
    
    # png svg
    Files: platformthemeplugin/icons/* styleplugins/chameleon/menu_shadow.svg styles/images/woodbackground.png
           styles/images/woodbutton.png tests/iconengines/builtinengine/icons/actions/icon_Layout_16px.svg
           tests/iconengines/svgiconengine/icon_window_16px.svg
    Copyright: None
    License: CC0-1.0
    
    # sh
    Files: tests/test-recoverage-qmake.sh
    Copyright: None
    License: CC0-1.0
    
    # ignore git
    Files: .git*
    Copyright: None
    License: CC0-1.0
    
    # xml toml json conf yaml
    Files: *.xml *.toml *.json *conf *.yaml
    Copyright: None
    License: CC0-1.0
    
    # rpm
    Files: rpm/*
    Copyright: None
    License: CC0-1.0
    
    # debian
    Files: debian/*
    Copyright: None
    License: CC0-1.0
    
    # Arch
    Files: archlinux/*
    Copyright: None
    License: CC0-1.0
    

    无需版权声明文件的处理方案

    有部分文件是无需版权声明的,比如说某些资源文件脚本文件或者序列化的文件,这种文件有两种处理方式:

    • .gitignore 是用来忽略某些与项目无关的文件和编译过程中产生的文件,同样的如果reuse也会忽略在.gitignore中标记的文件
    • dep5 在执行项目init之后,会在项目目录下产生.reuse/dep5 文件,打开后能看到官方给的范例,如果一个文件不需要特殊版权声明则使用CC0-1.0(会放弃对此文件的所有版权)

    检查项目合规

    reuse lint
    

    这个命令会列出项目文件数,如果项目已经合规,将会以:-)提示,如果项目有些文件不符合开源许可证规范将会以:-(提示,此时你就需要按照上面的提示进行相应的修改即可。

    一般情况下,每个文件都需要有与之相关的版权和许可信息。REUSE规范详细说明了几种方法。总的来说,有这些方法:

    • 将标签放在文件的标题中。
    • 将标签放置在与.license文件相邻的文件中。
    • 将信息放入 DEP5 文件中。

    如果发现一个文件没有与之关联的版权和/或许可信息,则该项目不合规。

    个人建议

    个人建议如果对现有项目使用reuse配置且需要保留版权的时间信息,不要使用一键配置版权头的方式,而是进行手动替换,这里推荐使用vscode的文件筛选功能,筛选出你需要添加的文件,然后cv下去。在配置dep5文件的时候,一定小心通配符的范围,不宜过大。并且对于第三方版权文件一定得小心规避,同样需要完整保留第三方版权信息。

    统信软件常见Dep5书写参考

    参考详细见:

    Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
    Upstream-Name: dtkgui
    Upstream-Contact: UnionTech Software Technology Co., Ltd. <>
    Source: https://github.com/linuxdeepin/dtkgui
    
    # ci
    Files: .github/* .gitlab-ci.yml
    Copyright: None
    License: CC0-1.0
    
    # gitignore #
    Files: .gitignore
    Copyright: None
    License: CC0-1.0
    
    # json conf yaml
    Files: *.json *conf *.yaml
    Copyright: None
    License: CC0-1.0
    
    #interface
    Files: src/util/D* src/kernel/D* src/filedrag/D*
    Copyright: None
    License: CC0-1.0
    
    # rpm
    Files: rpm/*
    Copyright: None
    License: CC0-1.0
    
    # debian
    Files: debian/*
    Copyright: None
    License: LGPL-3.0-or-later
    
    # Arch
    Files: archlinux/*
    Copyright: None
    License: CC0-1.0
    
    # README&doc
    Files: README.md doc/src/*.qdoc
    Copyright: None
    License:  CC-BY-4.0
    
    # DBus
    Files: src/dbus/*.xml
    Copyright: None
    License: CC0-1.0
    
    # Project file
    Files: *.pro *.prf *.pri *.qrc *CMakeLists.txt
    Copyright: None
    License: CC0-1.0
    
    # svg
    Files: src/util/icons/actions/*  src/util/icons/icons/* src/util/icons/texts/*
      tests/images/logo_icon.svg
    Copyright: UnionTech Software Technology Co., Ltd.
    License: LGPL-3.0-or-later
    

    常见开源协议选择

    一般情况下对于文档信息使用CC-BY-4.0 对于我们的一般开源项目使用LGPL-3.0-or-later许可证,如果遇到第三方文件,则是需要保留原有许可证,保留原版版权声明。

    Saturday, July 9, 2022

    这里整理一些关于c++中的匿名函数的知识.

    《C++ primer》上有的内容就不在重述了.这里重点讲一些primer上可能没有的东西.

    捕获的时机

    primer 向我们介绍到: 有两种捕获变量的方式, 值捕获和引用捕获. 其中:

    与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

    如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存 在的.lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了.

    我们看一段代码:

    auto funtionTimesMod1(int mod) {
      int variableA = mod;
      auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test1() {
      cout << "---test1---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod1(c);
      cout << times(a, b) << endl;
    }
    

    这里funtionTimesMod1(int mod)返回一个签名为int(int, int)的函数,这个函数计算 两个参数相乘对mod取模的结果.

    当然可以正常运行,结果为4,如果我们将它换成引用捕获:

    auto funtionTimesMod2(int mod) {
      int variableA = mod;
      auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test2() {
      cout << "---test2---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod2(c);
      cout << times(a, b) << endl;
    }
    

    这段代码就已经不能正常工作了,它的输出是 0.

    我们来看看为啥:

    Process 15820 stopped
    * thread #1, name = 'tmp', stop reason = step in
        frame #0: 0x000000000040096b tmp`funtionTimesMod2(mod=7) at tmp.cpp:17
       14   }
       15
       16   auto funtionTimesMod2(int mod) {
    -> 17     int variableA = mod;
       18     auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
       19     return f;
       20   }
    (lldb) p &variableA
    (int *) $1 = 0x00007fffffffe120
    
    Process 15820 stopped
    * thread #1, name = 'tmp', stop reason = step over
        frame #0: 0x00000000004009dc tmp`test2() at tmp.cpp:25
       22     cout << "---test2---" << endl;
       23     int a = 10, b = 20, c = 7;
       24     auto times = funtionTimesMod2(c);
    -> 25     cout << times(a, b) << endl;
       26   }
       27   auto funtionTimesMod3(int mod) {
       28     int variableA = mod;
    (lldb) p times
    ((anonymous class)) $2 = {
      variableA = 0x00007fffffffe120
    }
    

    可以看到times这个对象中保存下来的variableA只是一个指针,它指向我们之前创建的 局部变量variableA,这个地址在栈上,这意味着当我们真的调用times的时候,局部变 量variableA所在的那片内存已经被使用过了.所以会返回错误的结果.接下来,我们可以 看到实际上调用的时候variableA里面是20,这是因为刚好参数b被放置在 了variableA之前的位置上.

    Process 21271 stopped
    * thread #1, name = 'tmp', stop reason = step in
        frame #0: 0x0000000000400a32 tmp`funtionTimesMod2(this=0x00007fffffffe158, a=10, b=20)::$_1::operator()(int, int) const at tmp.cpp:18
       15
       16   auto funtionTimesMod2(int mod) {
       17     int variableA = mod;
    -> 18     auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
       19     return f;
       20   }
       21   void test2() {
    (lldb) p variableA
    (int) $3 = 20
    (lldb) p &b
    (int *) $4 = 0x00007fffffffe120
    

    lambda 的实现

    根据《C++ Primer》在10.3以及14.8.1的介绍,我们知道一个lambda表达式是由编译器负责 翻译成一个没有类名,重载过调用运算符的对象的.

    这解释了为什么我们只能用auto来定义一个lambda类型的变量,因为这个类是没有名字的, 或者说它的名字是编译器自己生成的,我们并不能知道它叫什么.

    这听起来很美好,我们也确实可以写出代码,用一个自己创建的,重载过调用运算符的对象,来 模拟一个lambda:

    class INT {
    public:
      int num;
      INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
      INT() = default;
    };
    
    auto funtionTimesMod3(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      auto f = [variableA](int a, int b) {
        cout << "----C" << endl;
        return a % variableA.num * b % variableA.num;
      };
      cout << "----D" << endl;
      return f;
    }
    
    void test3() {
      cout << "---test3---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod3(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    

    为了观察发生的值拷贝的时机,以确定我们自己写的这个类的行为和lambda真的完全一致,我 们可以自定义一个class,并且让他发生拷贝构造的时候打印一些信息.

    运行结果如下:

    ---test3---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    ----F
    ----C
    4
    ----G
    ---test4---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    ----F
    ----C
    4
    ----G
    

    这又一次说明了说明捕获引起的值拷贝发生在lambda被初始化的时候.

    看起来这两个东西的行为几乎一致, 但是真的是这样吗?

    编译器的优化

    编译器会对我们写的代码做出一些优化,以减少复制对象的次数.如果不了解这点可以看看 这篇博客.

    如果我们关闭返回值优化,那么运行的结果是这样的:

    ---test3---
    ----E
    ----A
    ----B
    Copy constructor
    Copy constructor
    ----D
    Copy constructor
    Copy constructor
    ----F
    ----C
    4
    ----G
    ---test4---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    Copy constructor
    Copy constructor
    ----F
    ----C
    4
    ----G
    

    可以看到我们自己写的类,少了一次拷贝构造.

    我们先来解释一下lambda为什么会发生这么多次拷贝构造.

    auto funtionTimesMod3(int mod) {
      INT variableA; // 5
      cout << "----A" << endl; // 6 
      variableA.num = mod; // 7
      cout << "----B" << endl; // 8
      auto f = [variableA](int a, int b) { 
        cout << "----C" << endl; // 14
        return a % variableA.num * b % variableA.num; //15
      }; // 9
      cout << "----D" << endl; // 10
      return f; // 11
    }
    
    void test3() {
      cout << "---test3---" << endl; // 1
      int a = 10, b = 20, c = 7; // 2
      cout << "----E" << endl; // 3
      auto times = funtionTimesMod3(c); // 4
      cout << "----F" << endl; // 12
      cout << times(a, b) << endl; // 13
      cout << "----G" << endl; //16
    }
    

    首先我们的程序是按照如上的顺序运行的.

    可以看到前两个拷贝构造发生在9,而第3、4次发生在11.

    如果完全按照语义来看的话,9这句话可以有两种理解方式:

    1. 创建一个lambda对象,对象名字叫f,这个对象的内容就是后面那个lambda表达式.
    2. 创建一个lambda表达式,然后将其作为参数,调用同类型对象f的拷贝构造函数.

    如果按照第一种方法来理解,那么第9行发生两次拷贝构造就不是很能理解了.

    所以应该是第二种.

    在10之后也发生了两次拷贝调用.应该是先建立了一个变量用来做返回值,比如说叫r,然后 将f赋值给返回值变量r,然后返回值变量r在被赋值给test3()中的times,这样发生的两次拷 贝构造.

    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA); // <- 这里
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    

    之前自己模拟lambda的这个代码,我们在注释标注的那个位置的实现和lambda中,“先建一个 右值,然后拷贝构造出f"的行为不太一样.导致这里少了一次拷贝构造.所以实际上应该是这 样:

    auto funtionTimesMod5(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      };
      function f = function(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test5() {
      cout << "---test5---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod5(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    

    实际上没必要纠结那么多,正常编译的话,其实是不会去先建一个右值的对象的.

    代码

    #include <bits/stdc++.h>
    using namespace std;
    
    auto funtionTimesMod1(int mod) {
      int variableA = mod;
      auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test1() {
      cout << "---test1---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod1(c);
      cout << times(a, b) << endl;
    }
    
    auto funtionTimesMod2(int mod) {
      int variableA = mod;
      auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test2() {
      cout << "---test2---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod2(c);
      cout << times(a, b) << endl;
    }
    
    class INT {
    public:
      int num;
      INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
      INT() = default;
    };
    
    auto funtionTimesMod3(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      auto f = [variableA](int a, int b) {
        cout << "----C" << endl;
        return a % variableA.num * b % variableA.num;
      };
      cout << "----D" << endl;
      return f;
    }
    
    void test3() {
      cout << "---test3---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod3(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod5(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      };
      function f = function(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test5() {
      cout << "---test5---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod5(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    int main() {
      test1();
      test2();
      test3();
      test4();
      test5();
    }
    

    Saturday, June 18, 2022

    Bcache 是 Linux 下的一个块缓存内核模块,可用于使用单个固态硬盘为一个或多个机械硬盘加速,也可用于使用本地磁盘为网络磁盘的加速。

    Bcache 有比较灵活的缓存模式,支持安全读写缓存(writethrough)、高性能读写缓存(writeback)、只读缓存(writearound)、停用缓存(none)等模式,并且可在线动态调整。

    Bcache 还会自动识别顺序写入,当发现正在进行顺序写入时跳过缓存层,以减少缓存设备(固态硬盘)的损耗,延长固态硬盘的寿命。

    Monday, May 30, 2022

    DDE 自启动管理插件可用于集中管理Deepin系统中软件开机自启动设置的插件。

    项目链接:DDE 自启动管理插件 github仓库

    参赛信息

    本项目参加2023全国大学生计算机系统能力大赛操作系统设计赛-功能挑战赛,选题为proj223-control-center-startup-management-plugin

    参赛小组:

    • 复旦大学 朱元依
    • 复旦大学 沈扬
    • 复旦大学 朱俊杰

    指导老师:

    • 复旦大学 张亮
    • 复旦大学 陈辰
    • 企业导师 王子冲

    完成情况

    项目文档位于根目录的初赛报告.md
    博客地址:https://y-aang.github.io (题目:DDE 自启动管理插件)

    任务完成情况
    (必须)完成一个控制中心插件,能够展示当前所有开机启动项的列表完成
    (必须)插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目完成
    (必须)能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项完成
    (必须)编写博客,记录开发过程的心得与体会,并将博客投递至 planet.deepin.org完成博客编写,待deepin官方审核

    成果展示

    功能展示视频链接:

    链接:百度网盘链接

    提取码:hoo8

    插件图标:

    图标

    自启动管理窗口:

    前端界面

    部署方式

    1、系统环境

    开发环境:Deepin 20Beta版

    系统架构:x86

    镜像下载链接:Deepin 操作系统下载链接

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:环境搭建博客

    2、安装依赖库

    安装包 build-essentialgitg++cmakeddedtk

    sudo apt install build-essential git g++ cmake
    sudo apt install dde-dock-dev libdtkwidget-dev
    

    3、安装插件

    克隆该仓库,进入文件夹后以sudo权限运行install.sh脚本

    git clone https://github.com/PinappleUnderTheSea/os223.git
    cd os233
    sudo su
    sh install.sh
    

    出现成果展示章节的图标,即为安装成功。

    提交仓库目录和文件描述

    .
    ├── CMakeLists.txt
    ├── README.md
    ├── aboutdialog.cpp    #关于窗口的实现文件
    ├── aboutdialog.h     #关于窗口的头文件
    ├── aboutdialog.ui     #关于窗口的UI文件
    ├── appletwidget.cpp   #自启动管理窗口的实现文件
    ├── appletwidget.h    #自启动管理窗口的头文件
    ├── images        #图片
    │ ├── QT_IDE.png
    │ ├── QT_前端.png
    │ ├── deepin自启动修改.png
    │ ├── 类图.jpg
    │ ├── 右键.png
    │ ├── 图标.png
    │ ├── 结果.png
    │ ├── 中期类图.jpg
    │ ├── 中期测试.png
    │ └── 前端界面.png
    ├── install.sh        #插件安装脚本
    ├── main_aboutdialog_test.cpp #关于窗口的测试文件
    ├── main_test.cpp      #测试文件
    ├── mainwidget.cpp     #插件类的实现文件
    ├── mainwidget.h      #插件类的头文件
    ├── self_startup.json     #插件的元数据文件,指明了当前插件所使用的 dde-dock 的接口版本
    ├── self_startup.pro     #辅助 cmake 的配置文件
    ├── self_startup.qrc     #用于展示插件图片
    ├── selfstartupplugin.cpp   #部件类的实现文件
    ├── selfstartupplugin.h    #部件类的头文件
    ├── uninstall.sh        #插件卸载脚本
    ├── 初赛报告.md
    └── 过程文档.md

    分工

    朱元依:插件类框架开发、部件类前端开发

    沈扬:自启动管理功能逻辑设计、插件类右键功能开发

    朱俊杰:自启动管理窗口后端接口开发(添加、删除、启用、禁用)

    开发计划

    第一步(4/9~4/18)

    • 调研Deepindde-dockQT框架等相关内容
    • 设计项目方案
    • 分工

    第二步(4/19~5/2)

    • 搭建主体插件类的框架
    • 设计启动项管理窗口的前端展示页面

    第三步(5/3~5/13)

    • 开发部件类接口
    • 完善插件类功能

    第四步(5/14~5/21)

    • 插件类右键功能开发
    • 完成配置文件

    第五步(5/22~5/31)

    • Debug
    • 撰写文档

    Thursday, March 10, 2022

    在项目开发中,我们通常会使用条件编译对代码进行裁剪,选择性地排除不需要的代码,比如在某个平台下完全不支持某个功能,那么这个功能就不应该被编译。

    一般我们使用宏来判断代码,选择性的挑选需要编译的部分,并在构建系统中开启这样的条件。

    #ifdef XXXXXXXXXX
        std::cout << "hello world!" << std::endl;
    #else
        std::cout << "good bye!" << std::endl;
    #endif
    

    在 C 语言的项目中,这样的行为是很正常的,甚至在 C++ 项目中,我们也会选择使用宏进行条件判断。

    但是如果定义的宏多了,则很容易导致代码碎片化,并且无法直观地看到工作流程。

    例如这样的代码:

    #define KWIN_VERSION_CHECK(major, minor, patch, build) ((major<<24)|(minor<<16)|(patch<<8)|build)
    #ifdef KWIN_VERSION_STR
    #define KWIN_VERSION KWIN_VERSION_CHECK(KWIN_VERSION_MAJ, KWIN_VERSION_MIN, KWIN_VERSION_PAT, KWIN_VERSION_BUI)
    #endif
    
    #if defined(KWIN_VERSION) && KWIN_VERSION < KWIN_VERSION_CHECK(5, 21, 3, 0)
    typedef int TimeArgType;
    #else
    typedef std::chrono::milliseconds TimeArgType;
    #endif
    
    #if defined(Q_OS_LINUX) && !defined(QT_NO_DYNAMIC_LIBRARY) && !defined(QT_NO_LIBRARY)
    QT_BEGIN_NAMESPACE
    QFunctionPointer qt_linux_find_symbol_sys(const char *symbol);
    QT_END_NAMESPACE
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        return QT_PREPEND_NAMESPACE(qt_linux_find_symbol_sys)(symbol);
    #else
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        static QString lib_name = "kwin.so." + qApp->applicationVersion();
    
        return QLibrary::resolve(lib_name, symbol);
    #endif
    }
    

    从上面的例子中可以看到,代码中一旦出现大量重复的判断条件,代码非常不直观,而且被宏分割成了很多部分。

    在一次偶然的机会,我看到了一篇介绍 C++ 17 中的 if constexpr 的用法,可以在编译期进行一些计算,虽然我很早就知道了 constexpr 的用法,但是大家举的例子基本上都是数值计算,让编译器在编译期间将数值进行计算,从而减轻运行时的消耗,我也从来想到其他用法,所以一直没有在项目中使用到。

    constexpr 的作用并不是编译期计算数值,而是编译期进行的代码分析,如果代码较小且非常直观,比如大家经常举的例子,在编译期间计算斐波那契数列,这种例子即使不使用 constexpr 显式要求,编译器也会帮助我们开启优化,直接给出结果。

    但是如果代码非常复杂,编译器就不一定会为我们做这样的优化,就需要我们手动标记可以计算的位置,要求编译器在编译期间进行求值和优化。

    我设想的是,使用 cmake 在构建时,先生成一份文件,将开关的值记录下来,在需要进行判断的地方,就可以直接使用 if constexpr 进行条件判断,在编译期间,编译器会发现有一个分支确定不会被执行(相当于 if(false) {}),那么这个分支就不会进行编译,直接剔除。

    CMakeLists.txt 中需要做一些工作,将编译参数加入构建系统。

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "1")
    else()
        set(ENABLE_MODULE "0")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    

    在 options/options.h.in 文件里,按照 cmake 的要求将变量导入进文件中,进行内容替换。

    #pragma once
    
    #cmakedefine01 ENABLE_MODULE
    

    这里我仍然使用的是宏定义,也可以直接写成如下形式:

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "true")
    else()
        set(ENABLE_MODULE "false")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    
    #pragma once
    
    const bool ENABLE_MODULE{@ENABLE_MODULE@};
    

    在 main.cpp 中写一段测试代码:

    #include "options/options.h"
    
    #include <iostream>
    
    int main() {
        if constexpr (ENABLE_MODULE) {
            std::cout << "Now Enable Module" << std::endl;
        }
    
        if constexpr (!ENABLE_MODULE) {
            std::cout << "Now Disable Module" << std::endl;
        }
    
        return 0;
    }
    

    执行结果是符合预期的。

    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:39]
    $ cmake ../ -G Ninja -DENABLE_MODULE=ON
    -- The CXX compiler identification is AppleClang 13.0.0.13000029
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:45]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:48]
    $ ./src/constexpr
    Now Enable Module
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:52]
    $ cmake ../ -G Ninja -DENABLE_MODULE=OFF
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:58]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:29:00]
    $ ./src/constexpr
    Now Disable Module
    

    虽然结果是符合的,但是我们其实不确定是否真的在编译期间就完成了代码剔除,所以使用命令进行汇编,查看汇编中是否包含了判断指令和两段输出的字符串。

    clang -S main.cpp -o main.s -I./
    

    main.s

    	.text
    	.file	"main.cpp"
    	.globl	main                            // -- Begin function main
    	.p2align	2
    	.type	main,@function
    main:                                   // @main
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, :got:_ZSt4cout
    	ldr	x19, [x19, :got_lo12:_ZSt4cout]
    	adrp	x1, .L.str
    	add	x1, x1, :lo12:.L.str
    	mov	w2, #18
    	mov	x0, x19
    	bl	_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
    	ldr	x8, [x19]
    	ldur	x8, [x8, #-24]
    	add	x8, x8, x19
    	ldr	x19, [x8, #240]
    	cbz	x19, .LBB0_5
    // %bb.1:
    	ldrb	w8, [x19, #56]
    	cbz	w8, .LBB0_3
    // %bb.2:
    	ldrb	w1, [x19, #67]
    	b	.LBB0_4
    .LBB0_3:
    	mov	x0, x19
    	bl	_ZNKSt5ctypeIcE13_M_widen_initEv
    	ldr	x8, [x19]
    	mov	w1, #10
    	mov	x0, x19
    	ldr	x8, [x8, #48]
    	blr	x8
    	mov	w1, w0
    .LBB0_4:
    	adrp	x0, :got:_ZSt4cout
    	ldr	x0, [x0, :got_lo12:_ZSt4cout]
    	bl	_ZNSo3putEc
    	bl	_ZNSo5flushEv
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	mov	w0, wzr
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	ret
    .LBB0_5:
    	bl	_ZSt16__throw_bad_castv
    .Lfunc_end0:
    	.size	main, .Lfunc_end0-main
    	.cfi_endproc
                                            // -- End function
    	.section	.text.startup,"ax",@progbits
    	.p2align	2                               // -- Begin function _GLOBAL__sub_I_main.cpp
    	.type	_GLOBAL__sub_I_main.cpp,@function
    _GLOBAL__sub_I_main.cpp:                // @_GLOBAL__sub_I_main.cpp
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, _ZStL8__ioinit
    	add	x19, x19, :lo12:_ZStL8__ioinit
    	mov	x0, x19
    	bl	_ZNSt8ios_base4InitC1Ev
    	adrp	x0, :got:_ZNSt8ios_base4InitD1Ev
    	ldr	x0, [x0, :got_lo12:_ZNSt8ios_base4InitD1Ev]
    	mov	x1, x19
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	adrp	x2, __dso_handle
    	add	x2, x2, :lo12:__dso_handle
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	b	__cxa_atexit
    .Lfunc_end1:
    	.size	_GLOBAL__sub_I_main.cpp, .Lfunc_end1-_GLOBAL__sub_I_main.cpp
    	.cfi_endproc
                                            // -- End function
    	.type	_ZStL8__ioinit,@object          // @_ZStL8__ioinit
    	.local	_ZStL8__ioinit
    	.comm	_ZStL8__ioinit,1,1
    	.hidden	__dso_handle
    	.type	.L.str,@object                  // @.str
    	.section	.rodata.str1.1,"aMS",@progbits,1
    .L.str:
    	.asciz	"Now Disable Module" // 关键在这里
    	.size	.L.str, 19
    
    	.section	.init_array,"aw",@init_array
    	.p2align	3
    	.xword	_GLOBAL__sub_I_main.cpp
    	.ident	"clang version 13.0.1"
    	.section	".note.GNU-stack","",@progbits
    	.addrsig
    	.addrsig_sym _GLOBAL__sub_I_main.cpp
    	.addrsig_sym _ZStL8__ioinit
    	.addrsig_sym __dso_handle
    	.addrsig_sym _ZSt4cout
    

    查看整个 main.s 汇编,发现只在 .L.str 段中有预期的文本字符串,可以得出结论,代码是在编译期间完成了剔除,符合我们的要求。

    原文链接:https://blog.justforlxz.com/2022/03/10/if-constexpr/