From 24d097376119e94c5a3db6ff3e4c5188baf05334 Mon Sep 17 00:00:00 2001 From: iloveeyjafjalla <186831707+iloveeyjafjalla@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:14:00 +0800 Subject: [PATCH] fix(repo): rewrite services exported to the systemd user directory --- .../src/linglong/repo/ostree_repo.cpp | 8 +- .../linglong/repo/ostree_repo_real_test.cpp | 81 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/libs/linglong/src/linglong/repo/ostree_repo.cpp b/libs/linglong/src/linglong/repo/ostree_repo.cpp index 3f97fc9cb..961a96ac8 100644 --- a/libs/linglong/src/linglong/repo/ostree_repo.cpp +++ b/libs/linglong/src/linglong/repo/ostree_repo.cpp @@ -2027,7 +2027,9 @@ utils::error::Result OSTreeRepo::exportDir(const std::string &appID, auto info = QFileInfo(target_path.c_str()); if ((info.path().contains("share/applications") && info.suffix() == "desktop") || (info.path().contains("share/dbus-1") && info.suffix() == "service") - || (info.path().contains("share/systemd/user") && info.suffix() == "service") + || ((info.path().contains("share/systemd/user") + || info.path().contains("lib/systemd/user")) + && info.suffix() == "service") || (info.path().contains("share/applications/context-menus"))) { // We should not modify the files of the checked application directly, but // should copy them and then modify. @@ -3232,7 +3234,9 @@ utils::error::Result OSTreeRepo::IniLikeFileRewrite(const QFileInfo &info, } else if (info.path().contains("share/dbus-1") && info.suffix() == "service") { return dbusServiceRewrite(info.absoluteFilePath(), id); - } else if (info.path().contains("share/systemd/user") && info.suffix() == "service") { + } else if ((info.path().contains("share/systemd/user") + || info.path().contains("lib/systemd/user")) + && info.suffix() == "service") { return systemdServiceRewrite(info.absoluteFilePath(), id); } else if (info.path().contains("share/applications/context-menus") && info.suffix() == "conf") { diff --git a/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_real_test.cpp b/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_real_test.cpp index 5fa951e18..e111cc36d 100644 --- a/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_real_test.cpp +++ b/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_real_test.cpp @@ -6,11 +6,16 @@ #include #include "../../common/tempdir.h" +#include "../mocks/ostree_repo_mock.h" +#include "configure.h" #include "linglong/api/types/v1/Generators.hpp" +#include "linglong/builder/builder_script.h" +#include "linglong/common/gkeyfile_wrapper.h" #include "linglong/package/fuzzy_reference.h" #include "linglong/package/layer_dir.h" #include "linglong/package/reference.h" #include "linglong/repo/ostree_repo.h" +#include "linglong/utils/cmd.h" #include #include @@ -353,6 +358,82 @@ TEST_F(RealRepoTest, CreateTempMergedModuleDirWorks) fs::remove_all(tmp->path(), ec); } +TEST_F(RealRepoTest, ExportSystemdServicesRewritesCommandsForBothLayouts) +{ + using KeyFile = common::GKeyFileWrapper; + auto generator = builder::findBuilderScript("app-conf-generator"); + ASSERT_TRUE(generator.has_value()) << generator.error().message(); + + // This wrapper only exposes exportDir; its callbacks remain unset. + MockOstreeRepo exporter(repoRoot, makeRepoConfig()); + for (const auto *layout : { "lib", "share" }) { + SCOPED_TRACE(layout); + const auto id = std::string("com.example.service.") + layout; + const auto stage = repoRoot / (std::string("service-source-") + layout); + const auto relative = fs::path(layout) / "systemd/user"; + const auto name = id + ".service"; + const auto service = stage / "files" / relative / name; + const auto start = "/opt/apps/" + id + "/files/bin/main"; + const auto post = "/opt/apps/" + id + "/files/bin/post"; + fs::create_directories(service.parent_path()); + std::ofstream(service) << "[Service]\nType=oneshot\nExecStart=" << start + << "\nExecStartPost=" << post << '\n'; + + auto generated = + utils::Cmd("bash").exec({ "-e", generator->string(), id, (stage / "files").string() }); + ASSERT_TRUE(generated.has_value()) << generated.error().message(); + auto before = KeyFile::New(QString::fromStdString(service.string())); + ASSERT_TRUE(before.has_value()) << before.error().message(); + auto generatedStart = before->getValue("ExecStart", KeyFile::SystemdService); + auto generatedPost = before->getValue("ExecStartPost", KeyFile::SystemdService); + ASSERT_TRUE(generatedStart.has_value()); + ASSERT_TRUE(generatedPost.has_value()); + ASSERT_EQ(*generatedStart, + QString::fromStdString("/usr/bin/ll-cli run " + id + " -- " + start)); + ASSERT_EQ(*generatedPost, QString::fromStdString(post)); + + fs::create_directories(stage / "entries" / relative); + fs::copy_file(service, stage / "entries" / relative / name); + PackageInfoV2 info; + info.id = id; + info.version = "1.0.0"; + info.kind = "app"; + info.channel = "stable"; + info.arch = { "x86_64" }; + info.packageInfoV2Module = "binary"; + std::ofstream(stage / "info.json") << nlohmann::json(info).dump(); + auto imported = repo->importLayerDir(LayerDir(stage)); + ASSERT_TRUE(imported.has_value()) << imported.error().message(); + auto ref = Reference::fromPackageInfo(info); + ASSERT_TRUE(ref.has_value()) << ref.error().message(); + auto layer = repo->getLayerDir(*ref, "binary"); + ASSERT_TRUE(layer.has_value()) << layer.error().message(); + const auto source = layer->path() / "entries" / relative; + ASSERT_GT(fs::hard_link_count(source / name), 1); + + const auto destination = repoRoot / "entries"; + const auto exportedPath = destination / "lib/systemd/user" / name; + const auto prefix = std::string(LINGLONG_CLIENT_PATH) + " run " + id + " -- "; + for (int pass = 0; pass < 2; ++pass) { + SCOPED_TRACE(pass); + auto exported = exporter.exportDir(id, source, destination, "lib/systemd/user", 10); + ASSERT_TRUE(exported.has_value()) << exported.error().message(); + ASSERT_TRUE(fs::is_symlink(exportedPath)); + auto after = KeyFile::New(QString::fromStdString(exportedPath.string())); + ASSERT_TRUE(after.has_value()) << after.error().message(); + auto exportedStart = after->getValue("ExecStart", KeyFile::SystemdService); + auto exportedPost = after->getValue("ExecStartPost", KeyFile::SystemdService); + auto type = after->getValue("Type", KeyFile::SystemdService); + ASSERT_TRUE(exportedStart.has_value()); + ASSERT_TRUE(exportedPost.has_value()); + ASSERT_TRUE(type.has_value()); + EXPECT_EQ(*exportedStart, QString::fromStdString(prefix + start)); + EXPECT_EQ(*exportedPost, QString::fromStdString(prefix + post)); + EXPECT_EQ(*type, "oneshot"); + } + } +} + TEST_F(RealRepoTest, ListLocalByQuery) { auto all = repo->listLocalBy(repoCacheQuery{ .id = "com.example.app" });