From 6af9b30ffc4551fed696c1909b4eadfe8f3ba8c6 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 10:34:21 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.zh_CN.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.zh_CN.md b/README.zh_CN.md index bc60bf4..3085147 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -1 +1,32 @@ -TODO: +# DDM + +`ddm` 项目是基于 `SDDM` 的显示管理器分支。 + +## 依赖项 + +检查 `debian/control` 中的构建时和运行时依赖项,或者使用 `cmake` 来检查缺失的所需依赖项。 +## Building + +常规的 CMake 构建步骤适用,简而言之: + +```shell +$ cmake -Bbuild +$ cmake --build build +$ cmake --install build # only do this if you know what you are doing +``` + +提供了一个 `debian` 文件夹,用于在 *deepin* Linux 桌面发行版下构建该软件包。 要构建该包,请使用以下命令: + +```shell +$ sudo apt build-dep . # install build dependencies +$ dpkg-buildpackage -uc -us -nc -b # build binary package(s) +``` + +## 参与方式 + +- [通过 GitHub 提交代码](https://github.com/linuxdeepin/ddm/) +- [向 GitHub 问题或 GitHub 讨论中提交错误或建议](https://github.com/linuxdeepin/developer-center/issues/new/choose) + +## 许可证 + +**ddm** 采用 GPL-2.0+ 许可证。有关详细信息,请参阅 REUSE 文件。 \ No newline at end of file From 1a9416f74a06b3558db62f3dcd958c52dcc2553a Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 10:35:48 +0800 Subject: [PATCH 02/10] update auth --- src/daemon/Auth.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/daemon/Auth.cpp b/src/daemon/Auth.cpp index ee467a4..2a3675c 100644 --- a/src/daemon/Auth.cpp +++ b/src/daemon/Auth.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -319,6 +320,19 @@ namespace DDM { env.insert(QStringLiteral("LOGNAME"), QString::fromLocal8Bit(pw->pw_name)); } + // PAM modules (e.g. pam_gnome_keyring) may fork() internally and + // call exit() instead of _exit() in the fork child, which triggers + // Qt's atexit cleanup (including libQt6DBus joining its dispatcher + // thread). After fork(), that thread doesn't exist, so the join + // blocks forever on a futex/semaphore. + // + // Register a pthread_atfork child handler here so that any + // grandchild processes spawned by PAM will have an atexit handler + // that runs _exit() first (LIFO), bypassing Qt's broken cleanup. + pthread_atfork(nullptr, nullptr, []() { + atexit([]() { _exit(0); }); + }); + // Open session auto sessionEnv = openSessionInternal(env); if (!sessionEnv.has_value()) { From f92258a5abfca03bd0361fad3e96b767851eb0ed Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:21:22 +0800 Subject: [PATCH 03/10] update --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 21ddaff..9d910f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ include(FeatureSummary) # Options option(BUILD_MAN_PAGES "Build man pages" OFF) +option(BUILD_TESTING "Build unit tests" OFF) option(WERROR "Build with -Werror" OFF) set(CMAKE_CXX_STANDARD 20) @@ -163,6 +164,11 @@ add_subdirectory(data) add_subdirectory(services) add_subdirectory(src) +if(BUILD_TESTING) + enable_testing() + add_subdirectory(test) +endif() + # Display feature summary feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) From a9fbf5863aa17557bab374b3c8cb54046eb91b36 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:22:16 +0800 Subject: [PATCH 04/10] auth --- src/daemon/Auth.cpp | 56 +++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/daemon/Auth.cpp b/src/daemon/Auth.cpp index 2a3675c..d7847b3 100644 --- a/src/daemon/Auth.cpp +++ b/src/daemon/Auth.cpp @@ -7,6 +7,7 @@ #include "DaemonApp.h" #include "DdeSeatdControl.h" +#include "ForkExitGuard.h" #include "Login1Manager.h" #include "Login1Session.h" #include "SignalHandler.h" @@ -15,7 +16,6 @@ #include #include -#include #include #include #include @@ -282,6 +282,26 @@ namespace DDM { return -1; } + // Install the forked-child exit bypass here, in the daemon, while + // libc locks are still consistent and atexit() is safe to call. + // + // Exit handlers are inherited across fork(): without the bypass, a + // forked child calling exit() would run the daemon's cleanup + // handlers (e.g. libQt6DBus joining its dispatcher thread), which + // deadlock because those threads do not exist after fork(). The + // bypass is registered last, so it runs first (LIFO) in every + // descendant -- grandchildren forked by PAM modules (e.g. + // pam_gnome_keyring) and QProcess children included -- and _exit(0)s + // before any inherited handler is reached. The daemon itself is + // excluded via the owner PID check and keeps its regular cleanup. + // + // This must never be done from a pthread_atfork child handler: + // atexit() is not async-signal-safe and may block forever on libc + // internal locks inherited in a locked state from another thread of + // the forking process. + if (!ForkExitGuard::install()) + qWarning() << "[Auth] Failed to install the forked-child exit bypass"; + sessionLeaderPid = fork(); switch (sessionLeaderPid) { case -1: { @@ -320,24 +340,20 @@ namespace DDM { env.insert(QStringLiteral("LOGNAME"), QString::fromLocal8Bit(pw->pw_name)); } - // PAM modules (e.g. pam_gnome_keyring) may fork() internally and - // call exit() instead of _exit() in the fork child, which triggers - // Qt's atexit cleanup (including libQt6DBus joining its dispatcher - // thread). After fork(), that thread doesn't exist, so the join - // blocks forever on a futex/semaphore. - // - // Register a pthread_atfork child handler here so that any - // grandchild processes spawned by PAM will have an atexit handler - // that runs _exit() first (LIFO), bypassing Qt's broken cleanup. - pthread_atfork(nullptr, nullptr, []() { - atexit([]() { _exit(0); }); - }); + // Neither this process nor anything forked from it may run the + // exit handlers inherited from the daemon: they were registered + // before fork() and may wait on threads that no longer exist + // (e.g. libQt6DBus joining its dispatcher thread blocks forever + // after fork()). The session leader therefore terminates with + // _exit() directly, and descendants forked by PAM modules which + // call exit() (e.g. pam_gnome_keyring) are terminated by the + // ForkExitGuard handler the daemon installed before fork(). // Open session auto sessionEnv = openSessionInternal(env); if (!sessionEnv.has_value()) { qCritical() << "[SessionLeader] Failed to open session. Exit now."; - exit(1); + _exit(1); } env = *sessionEnv; @@ -345,11 +361,11 @@ namespace DDM { xdgSessionId = env.value(QStringLiteral("XDG_SESSION_ID")).toInt(); if (xdgSessionId <= 0) { qCritical() << "[SessionLeader] Invalid XDG_SESSION_ID from pam_open_session()"; - exit(1); + _exit(1); } if (write(pipefd[1], &xdgSessionId, sizeof(int)) != sizeof(int)) { qCritical() << "[SessionLeader] Failed to write XDG_SESSION_ID to parent process!"; - exit(1); + _exit(1); } // RUN!!! @@ -358,14 +374,14 @@ namespace DDM { session.start(command, type, cookie); if (!session.waitForStarted()) { qCritical() << "[SessionLeader] Failed to start session process. Exit now."; - exit(1); + _exit(1); } // Send session PID to parent sessionPid = session.processId(); if (write(pipefd[1], &sessionPid, sizeof(qint64)) != sizeof(qint64)) { qCritical() << "[SessionLeader] Failed to write session PID to parent process!"; - exit(1); + _exit(1); } qInfo() << "[SessionLeader] Session started with PID" << sessionPid; @@ -374,11 +390,11 @@ namespace DDM { // Handle session end if (session.exitStatus() == QProcess::CrashExit) { qCritical() << "[SessionLeader] Session process crashed. Exit now."; - exit(1); + _exit(1); } qInfo() << "[SessionLeader] Session process finished with exit code" << session.exitCode() << ". Exiting."; - exit(session.exitCode()); + _exit(session.exitCode()); } default: { // Parent process From 371072e3a084753674202daeed70ddb75a766c79 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:23:18 +0800 Subject: [PATCH 05/10] add --- src/daemon/ForkExitGuard.h | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/daemon/ForkExitGuard.h diff --git a/src/daemon/ForkExitGuard.h b/src/daemon/ForkExitGuard.h new file mode 100644 index 0000000..9e88fc4 --- /dev/null +++ b/src/daemon/ForkExitGuard.h @@ -0,0 +1,71 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef DDM_FORKEXITGUARD_H +#define DDM_FORKEXITGUARD_H + +#include +#include +#include + +namespace DDM::ForkExitGuard { + /** PID of the process which installed the exit bypass. Forked children + * inherit this value unchanged, so inside a child it still refers to + * the original (daemon) process. -1 means "not installed". */ + inline pid_t s_ownerPid{ -1 }; + + /** + * atexit(3) handler terminating every process except the one which + * installed it (i.e. every fork()ed descendant) immediately with + * _exit(0), so that exit() in a forked child skips all the other + * exit handlers. + * + * Exit handlers registered via atexit()/__cxa_atexit() (such as Qt's + * static cleanup, e.g. libQt6DBus joining its dispatcher thread) are + * inherited across fork(), but the threads and other resources they + * operate on are not: running them in a forked child can deadlock + * forever. Exit handlers run in LIFO order, so this handler bypasses + * every handler registered before the last install() call. + */ + inline void bypassInheritedCleanup() { + if (::getpid() != s_ownerPid) + ::_exit(0); + } + + /** + * Installs (or refreshes) the exit bypass for forked children. + * + * Must be called from the original daemon process before fork(), + * where all libc locks are in a consistent state. It must never be + * called from a pthread_atfork child handler or any other post-fork + * child context: atexit() is not async-signal-safe and may block + * forever acquiring libc internal locks which were inherited in a + * locked state from another thread of the forking process. + * + * Calling this again before every fork() re-registers the handler at + * the end of the exit handler list, so that it also runs before any + * handler registered since the previous install(). The duplicate + * registrations are harmless in the owner process, where the handler + * is a no-op and the regular cleanup runs unchanged. + * + * @return true on success, false if the handler could not be registered + */ + inline bool install() { + if (::atexit(bypassInheritedCleanup) != 0) + return false; + s_ownerPid = ::getpid(); + return true; + } + + /** Whether the exit bypass has been installed in this process image. */ + inline bool isInstalled() { + return s_ownerPid != -1; + } + + /** PID of the process which installed the bypass, -1 if not installed. */ + inline pid_t ownerPid() { + return s_ownerPid; + } +} + +#endif // DDM_FORKEXITGUARD_H From 7581604544f1fde62956959092310618a594e2fd Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:25:54 +0800 Subject: [PATCH 06/10] Create test --- test | 1 + 1 file changed, 1 insertion(+) create mode 100644 test diff --git a/test b/test new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test @@ -0,0 +1 @@ + From 76e3017c15e4f2185983ee76f66a8cf1637726a0 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:26:43 +0800 Subject: [PATCH 07/10] Delete test --- test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test diff --git a/test b/test deleted file mode 100644 index 8b13789..0000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ - From fe8f868e6c5964ded34e0dbc7728d70b0b78c528 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:30:37 +0800 Subject: [PATCH 08/10] Add CMake configuration for tst_forkexitguard test --- test/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/CMakeLists.txt diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..47ca103 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Qt6 CONFIG REQUIRED Test) + +add_executable(tst_forkexitguard + tst_forkexitguard.cpp +) + +target_include_directories(tst_forkexitguard PRIVATE + "${CMAKE_SOURCE_DIR}/src/daemon" +) + +target_link_libraries(tst_forkexitguard PRIVATE + Qt6::Test +) + +add_test(NAME tst_forkexitguard COMMAND tst_forkexitguard) From ba8aeb76b4a6ff54e679f5ec71596f11d00f25ff Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:31:03 +0800 Subject: [PATCH 09/10] add --- test/tst_forkexitguard.cpp | 278 +++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 test/tst_forkexitguard.cpp diff --git a/test/tst_forkexitguard.cpp b/test/tst_forkexitguard.cpp new file mode 100644 index 0000000..433d6f3 --- /dev/null +++ b/test/tst_forkexitguard.cpp @@ -0,0 +1,278 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "ForkExitGuard.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace DDM; + +namespace { + constexpr char selfTestEnv[] = "DDM_FORKEXITGUARD_SELFTEST"; + + // Canary exit handler used to verify which handlers get to run before + // ForkExitGuard bypasses them. When armed, it writes one byte into a + // pipe watched by the test process; an armed write from a forked child + // means the inherited handler was NOT bypassed. + int s_canaryFd = -1; + bool s_canaryArmed = false; + + void canaryExitHandler() { + if (s_canaryArmed && s_canaryFd >= 0) { + const char marker = 'C'; + const ssize_t written = ::write(s_canaryFd, &marker, 1); + Q_UNUSED(written); + } + } + + // Disarm the canary and forget its pipe write end (parent side only). + void disarmCanary() { + s_canaryArmed = false; + s_canaryFd = -1; + } + + int waitChild(pid_t pid) { + int status = -1; + while (::waitpid(pid, &status, 0) == -1 && errno == EINTR) { } + return status; + } + + // Run this test binary again as a helper process in the given self-test + // mode and return it via outProc (already finished). + bool runSelfTest(const char *mode, QProcess &outProc) { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QLatin1String(selfTestEnv), QLatin1String(mode)); + outProc.setProcessEnvironment(env); + outProc.setProgram(QCoreApplication::applicationFilePath()); + outProc.start(); + if (!outProc.waitForStarted(5000)) + return false; + return outProc.waitForFinished(10000); + } +} + +class TestForkExitGuard : public QObject { + Q_OBJECT +private Q_SLOTS: + void testInitialState(); + void testInstall(); + void testHandlerIsNoOpInOwnerProcess(); + void testHandlerExitsInForkedChild(); + void testChildExitBypassesOlderHandlers(); + void testReinstallSupersedesNewerHandlers(); + void testOwnerExitPreservesNormalCleanup(); + void testDescendantExitIsBypassed(); + void testHandlerNoOpInOwnerSubprocess(); +}; + +void TestForkExitGuard::testInitialState() +{ + // Must run first: a fresh process image has no guard installed yet. + QVERIFY(!ForkExitGuard::isInstalled()); + QCOMPARE(ForkExitGuard::ownerPid(), static_cast(-1)); +} + +void TestForkExitGuard::testInstall() +{ + QVERIFY(ForkExitGuard::install()); + QVERIFY(ForkExitGuard::isInstalled()); + QCOMPARE(ForkExitGuard::ownerPid(), getpid()); + + // Installing again refreshes the registration (moves the handler to the + // end of the LIFO list) and must keep the owner PID unchanged. + QVERIFY(ForkExitGuard::install()); + QVERIFY(ForkExitGuard::isInstalled()); + QCOMPARE(ForkExitGuard::ownerPid(), getpid()); +} + +void TestForkExitGuard::testHandlerIsNoOpInOwnerProcess() +{ + // In the owner process the handler must simply return, so the regular + // exit cleanup keeps running. If it wrongly _exit()ed, this test process + // would die here; testHandlerNoOpInOwnerSubprocess pins that case down + // from the outside. + QVERIFY(ForkExitGuard::isInstalled()); + ForkExitGuard::bypassInheritedCleanup(); + QVERIFY(ForkExitGuard::isInstalled()); +} + +void TestForkExitGuard::testHandlerExitsInForkedChild() +{ + QVERIFY(ForkExitGuard::isInstalled()); + + const pid_t pid = fork(); + QVERIFY(pid >= 0); + if (pid == 0) { + // Child: our PID differs from the recorded owner, so the handler + // must terminate us immediately with status 0. + ForkExitGuard::bypassInheritedCleanup(); + // Only reached if the handler wrongly returned. + _exit(123); + } + + const int status = waitChild(pid); + QVERIFY(WIFEXITED(status)); + QCOMPARE(WEXITSTATUS(status), 0); +} + +void TestForkExitGuard::testChildExitBypassesOlderHandlers() +{ + int pipeFd[2]; + QCOMPARE(pipe(pipeFd), 0); + + // Register the canary BEFORE installing the guard: it models daemon-era + // handlers (e.g. Qt static cleanup) registered before fork(). The guard + // is then the newest handler and must run first (LIFO) in the child. + s_canaryFd = pipeFd[1]; + s_canaryArmed = true; + QCOMPARE(atexit(canaryExitHandler), 0); + QVERIFY(ForkExitGuard::install()); + + const pid_t pid = fork(); + QVERIFY(pid >= 0); + if (pid == 0) { + close(pipeFd[0]); + ::exit(42); + } + close(pipeFd[1]); + disarmCanary(); + + const int status = waitChild(pid); + QVERIFY(WIFEXITED(status)); + // The guard flattened exit(42) into _exit(0). + QCOMPARE(WEXITSTATUS(status), 0); + + // The canary must not have run in the child, i.e. nothing was written. + struct pollfd pfd {}; + pfd.fd = pipeFd[0]; + pfd.events = POLLIN; + QCOMPARE(poll(&pfd, 1, 0), 0); + close(pipeFd[0]); +} + +void TestForkExitGuard::testReinstallSupersedesNewerHandlers() +{ + int pipeFd[2]; + QCOMPARE(pipe(pipeFd), 0); + + // Register the canary AFTER the previous install(), then install() + // again: handlers registered between two fork()s must also be bypassed + // in children, because the refreshed registration runs first (LIFO). + s_canaryFd = pipeFd[1]; + s_canaryArmed = true; + QCOMPARE(atexit(canaryExitHandler), 0); + QVERIFY(ForkExitGuard::install()); + + const pid_t pid = fork(); + QVERIFY(pid >= 0); + if (pid == 0) { + close(pipeFd[0]); + ::exit(7); + } + close(pipeFd[1]); + disarmCanary(); + + const int status = waitChild(pid); + QVERIFY(WIFEXITED(status)); + QCOMPARE(WEXITSTATUS(status), 0); + + struct pollfd pfd {}; + pfd.fd = pipeFd[0]; + pfd.events = POLLIN; + QCOMPARE(poll(&pfd, 1, 0), 0); + close(pipeFd[0]); +} + +void TestForkExitGuard::testOwnerExitPreservesNormalCleanup() +{ + // Subprocess: installs the guard, registers a canary and exit(7)s. + // The owner process must keep its regular cleanup (canary runs) and + // its exit code (the guard must not fire for the owner). + QProcess proc; + QVERIFY(runSelfTest("owner-exit", proc)); + QCOMPARE(proc.exitStatus(), QProcess::NormalExit); + QCOMPARE(proc.exitCode(), 7); + QVERIFY(QString::fromLocal8Bit(proc.readAllStandardOutput()).contains(QStringLiteral("CANARY"))); +} + +void TestForkExitGuard::testDescendantExitIsBypassed() +{ + // Subprocess: installs the guard, forks, and the child exit(9)s. + // The child inherits the guard, so it must exit with status 0 without + // running any inherited cleanup; the harness prints the child's code. + QProcess proc; + QVERIFY(runSelfTest("descendant-exit", proc)); + QCOMPARE(proc.exitStatus(), QProcess::NormalExit); + QCOMPARE(proc.exitCode(), 0); + QCOMPARE(QString::fromLocal8Bit(proc.readAllStandardOutput()).trimmed(), QStringLiteral("0")); +} + +void TestForkExitGuard::testHandlerNoOpInOwnerSubprocess() +{ + // Subprocess: installs the guard and calls the handler directly. + // A wrongly firing guard would kill the harness silently with status 0, + // so success is proven by the SURVIVED marker on stdout. + QProcess proc; + QVERIFY(runSelfTest("owner-noop", proc)); + QCOMPARE(proc.exitStatus(), QProcess::NormalExit); + QCOMPARE(proc.exitCode(), 0); + QVERIFY(QString::fromLocal8Bit(proc.readAllStandardOutput()).contains(QStringLiteral("SURVIVED"))); +} + +int main(int argc, char *argv[]) +{ + const QByteArray selfTest = qgetenv(selfTestEnv); + + if (selfTest == "owner-noop") { + if (!ForkExitGuard::install()) + return 1; + // Must return instead of terminating the process. + ForkExitGuard::bypassInheritedCleanup(); + puts("SURVIVED"); + return 0; + } + + if (selfTest == "owner-exit") { + // Registered before the guard, so it runs after the guard (LIFO): + // reaching it proves the guard did not fire for the owner. + atexit([]() { + puts("CANARY"); + }); + if (!ForkExitGuard::install()) + return 1; + ::exit(7); + } + + if (selfTest == "descendant-exit") { + if (!ForkExitGuard::install()) + return 1; + const pid_t pid = fork(); + if (pid == 0) + ::exit(9); + if (pid < 0) + return 1; + int status = -1; + while (::waitpid(pid, &status, 0) == -1 && errno == EINTR) { } + if (!WIFEXITED(status)) + return 1; + printf("%d\n", WEXITSTATUS(status)); + return 0; + } + + QCoreApplication app(argc, argv); + TestForkExitGuard tc; + return QTest::qExec(&tc, argc, argv); +} + +#include "tst_forkexitguard.moc" From ce3c716d2842a3e2d035de0e258e5ef9beb25647 Mon Sep 17 00:00:00 2001 From: lchjvvj Date: Sat, 26 Sep 2026 11:37:15 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate installation instruction comment to Chinese. --- README.zh_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.zh_CN.md b/README.zh_CN.md index 3085147..b91cc27 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -12,7 +12,7 @@ ```shell $ cmake -Bbuild $ cmake --build build -$ cmake --install build # only do this if you know what you are doing +$ cmake --install build # 只有在你知道自己在做什么的情况下才这样做。 ``` 提供了一个 `debian` 文件夹,用于在 *deepin* Linux 桌面发行版下构建该软件包。 要构建该包,请使用以下命令: @@ -29,4 +29,4 @@ $ dpkg-buildpackage -uc -us -nc -b # build binary package(s) ## 许可证 -**ddm** 采用 GPL-2.0+ 许可证。有关详细信息,请参阅 REUSE 文件。 \ No newline at end of file +**ddm** 采用 GPL-2.0+ 许可证。有关详细信息,请参阅 REUSE 文件。