Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/ll-builder/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,10 @@ You can report bugs to the linyaps team under this project: https://github.com/O
->delimiter(',')
->allow_extra_args(false)
->type_name("modules");
buildRun->add_option("--workdir", runOpts.workdir, _("Specify the working directory where the application runs"))
buildRun
->add_option("--workdir",
runOpts.workdir,
_("Specify the working directory where the application runs"))
->type_name("PATH");
buildRun->add_option(
"COMMAND",
Expand Down
27 changes: 18 additions & 9 deletions libs/linglong/src/linglong/cli/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1668,31 +1668,40 @@ int Cli::content(const ContentOptions &options)
return -1;
}

QDir entriesDir((layer->path() / "entries/share").c_str());
QDir entriesDir((layer->path() / "entries").c_str());
if (!entriesDir.exists()) {
this->printer.printErr(LINGLONG_ERR("no entries found").value());
return -1;
}

const auto preferLibSystemdUser = QFileInfo(entriesDir.filePath("lib/systemd/user")).exists();

QDirIterator it(entriesDir.absolutePath(),
QDir::AllEntries | QDir::NoDot | QDir::NoDotDot | QDir::System,
QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
contents.append(it.fileInfo().absoluteFilePath());
const auto entryPath = it.fileInfo().absoluteFilePath();
const auto relativePath =
std::filesystem::path(entriesDir.relativeFilePath(entryPath).toStdString());
const auto exportPath =
this->repository.resolveEntryExportPath(relativePath, preferLibSystemdUser);
if (!exportPath.empty()) {
contents.append(QString::fromStdString(exportPath.string()));
}
}
// replace $LINGLONG_ROOT/layers/appid/verison/arch/module/entries to ${LINGLONG_ROOT}/entires
contents.replaceInStrings(entriesDir.absolutePath(), QString(LINGLONG_ROOT) + "/entries/share");

// only show the contents which are exported
for (int pos = 0; pos < contents.size(); ++pos) {
QFileInfo info(contents.at(pos));
if (!info.exists()) {
contents.removeAt(pos);
QStringList exportedContents{};
for (const auto &content : std::as_const(contents)) {
QFileInfo info(content);
if (!info.exists() || info.isDir()) {
continue;
}
exportedContents.append(content);
}

this->printer.printContent(contents);
this->printer.printContent(exportedContents);
return 0;
}

Expand Down
61 changes: 61 additions & 0 deletions libs/linglong/src/linglong/repo/ostree_repo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3054,6 +3054,67 @@ QDir OSTreeRepo::getDefaultSharedDir() const noexcept
return this->repoDir.absoluteFilePath("entries/share");
}

std::filesystem::path OSTreeRepo::resolveEntryExportPath(const std::filesystem::path &relativePath,
bool preferLibSystemdUser) const noexcept
{
auto relative = relativePath.generic_string();
if (relative.empty()) {
return {};
}

const auto repoDirPath = std::filesystem::path(this->repoDir.absolutePath().toStdString());
const auto entriesDir = repoDirPath / "entries";

if (common::strings::starts_with(relative, "share/applications/")
&& common::strings::ends_with(relative, ".desktop")) {
return this->resolveDesktopFileExportPath(
std::filesystem::path(relative).lexically_relative(std::filesystem::path{ "share" }));
}

// exportEntries() will export systemd user units to entries/lib/systemd/user:
// 1. Prefer entries/lib/systemd/user when the app ships it directly.
// 2. Fall back to entries/share/systemd/user only for legacy apps, but still map the
// resulting visible path to entries/lib/systemd/user.
// Returning an empty path here skips the legacy share path when lib/systemd/user exists,
// so `ll-cli content` does not emit the same exported target twice.
constexpr std::string_view shareSystemdUser = "share/systemd/user";
if (relative == shareSystemdUser
|| common::strings::starts_with(relative, "share/systemd/user/")) {
if (preferLibSystemdUser) {
return {};
}

auto suffix = relative.substr(shareSystemdUser.size());
return entriesDir / std::filesystem::path("lib/systemd/user" + suffix);
}

constexpr std::string_view libSystemdUser = "lib/systemd/user";
if (relative == libSystemdUser || common::strings::starts_with(relative, "lib/systemd/user/")) {
return entriesDir / relativePath;
}

return entriesDir / relativePath;
}

std::filesystem::path
OSTreeRepo::resolveDesktopFileExportPath(const std::filesystem::path &relativePath) const noexcept
{
const auto defaultDesktopPath =
std::filesystem::path(this->getDefaultSharedDir().absolutePath().toStdString())
/ relativePath;
const auto overlayDesktopPath =
std::filesystem::path(this->getOverlayShareDir().absolutePath().toStdString()) / relativePath;
if (overlayDesktopPath != defaultDesktopPath && std::filesystem::exists(overlayDesktopPath)) {
return overlayDesktopPath;
}

if (std::filesystem::exists(defaultDesktopPath)) {
return defaultDesktopPath;
}

return overlayDesktopPath;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The resolveDesktopFileExportPath method returns overlayDesktopPath even if neither the overlay nor the default desktop file exists. While the calling code in cli.cpp filters out non-existent paths, it might be more explicit for this method to return an empty QString or an std::optional<QString> if no existing file is found. This would clearly communicate that no valid, existing path was resolved, making the method's contract more robust for future uses.

}

QDir OSTreeRepo::getOverlayShareDir() const noexcept
{
return this->repoDir.absoluteFilePath("entries/" LINGLONG_EXPORT_PATH);
Expand Down
5 changes: 5 additions & 0 deletions libs/linglong/src/linglong/repo/ostree_repo.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@

#include <ostree.h>

#include <QDir>

Check warning on line 24 in libs/linglong/src/linglong/repo/ostree_repo.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <filesystem>

Check warning on line 26 in libs/linglong/src/linglong/repo/ostree_repo.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <memory> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <memory>

Check warning on line 27 in libs/linglong/src/linglong/repo/ostree_repo.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <optional> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <optional>

Check warning on line 28 in libs/linglong/src/linglong/repo/ostree_repo.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <string> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <string>

Check warning on line 29 in libs/linglong/src/linglong/repo/ostree_repo.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <vector> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <vector>

namespace linglong::repo {
Expand Down Expand Up @@ -190,6 +191,10 @@
std::string module = "binary",
const std::optional<std::string> &subRef = std::nullopt) const noexcept;
utils::error::Result<void> fixExportAllEntries() noexcept;
[[nodiscard]] std::filesystem::path resolveEntryExportPath(
const std::filesystem::path &relativePath, bool preferLibSystemdUser = false) const noexcept;
[[nodiscard]] std::filesystem::path
resolveDesktopFileExportPath(const std::filesystem::path &relativePath) const noexcept;

virtual std::unique_ptr<ClientAPIWrapper> createClientV2(const std::string &url) const
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 205-2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

Expand Down Expand Up @@ -99,9 +99,8 @@ TEST_F(ApplicationSingletonTest, ReleasingLockManuallyWorks)
TEST_F(ApplicationSingletonTest, CreatesDirectoryForLockFile)
{
TempDir temp_dir;
auto lockFilePath = temp_dir.path() / "ll_driver_detect_singleton_test.lock";
using namespace linglong::driver::detect;
auto deepLockPath = std::filesystem::temp_directory_path() / "deep_dir_for_test" / "test.lock";
auto deepLockPath = temp_dir.path() / "deep_dir_for_test" / "test.lock";
std::filesystem::remove(deepLockPath);
std::filesystem::remove(deepLockPath.parent_path());

Expand Down
162 changes: 162 additions & 0 deletions libs/linglong/tests/ll-tests/src/linglong/cli/cli_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
#include "linglong/cli/cli.h"
#include "linglong/cli/cli_printer.h"
#include "linglong/cli/dummy_notifier.h"
#include "linglong/cli/printer.h"

Check warning on line 14 in libs/linglong/tests/ll-tests/src/linglong/cli/cli_test.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/cli/printer.h" not found.
#include "ocppi/cli/crun/Crun.hpp"

Check warning on line 15 in libs/linglong/tests/ll-tests/src/linglong/cli/cli_test.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "ocppi/cli/crun/Crun.hpp" not found.

#include <filesystem>
#include <fstream>

using namespace linglong;
using ::testing::_;
using ::testing::ElementsAre;
Expand All @@ -32,10 +35,27 @@
{
}

QDir defaultSharedDir() const noexcept { return this->getDefaultSharedDir(); }

QDir overlaySharedDir() const noexcept { return this->getOverlayShareDir(); }

MOCK_METHOD(utils::error::Result<std::vector<api::types::v1::PackageInfoV2>>,
listLocal,
(),
(override, const, noexcept));
MOCK_METHOD(utils::error::Result<package::Reference>,
clearReference,
(const package::FuzzyReference &fuzzyRef,
const repo::clearReferenceOption &opts,
const std::string &module,
const std::optional<std::string> &repo),
(override, const, noexcept));
MOCK_METHOD(utils::error::Result<api::types::v1::RepositoryCacheLayersItem>,
getLayerItem,
(const package::Reference &ref,
std::string module,
const std::optional<std::string> &subRef),
(override, const, noexcept));
MOCK_METHOD(utils::error::Result<package::ReferenceWithRepo>,
latestRemoteReference,
(const package::FuzzyReference &fuzzyRef),
Expand All @@ -49,6 +69,7 @@
printUpgradeList,
(std::vector<api::types::v1::UpgradeListResult> &),
(override));
MOCK_METHOD(void, printContent, (const QStringList &filePaths), (override));
};

class CliTest : public ::testing::Test
Expand Down Expand Up @@ -237,4 +258,145 @@
cli->list(cli::ListOptions{ .showUpgradeList = true });
}

TEST_F(CliTest, contentPreferDesktopFromDefaultSharedDir)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-default";
const auto layerEntriesDir =
tempDir->path() / "layers" / commit / "entries" / "share" / "applications";
const auto defaultDesktopPath =
std::filesystem::path(repo->defaultSharedDir().absolutePath().toStdString()) / "applications"
/ "org.example.app.desktop";

std::filesystem::create_directories(layerEntriesDir);
std::filesystem::create_directories(defaultDesktopPath.parent_path());
std::ofstream(layerEntriesDir / "org.example.app.desktop") << "Test desktop content";
std::ofstream(defaultDesktopPath) << "Test desktop content in default";

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer,
printContent(ElementsAre(QString::fromStdString(defaultDesktopPath.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentFallbackDesktopToOverlaySharedDir)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-overlay";
const auto layerEntriesDir =
tempDir->path() / "layers" / commit / "entries" / "share" / "applications";
const auto overlayDesktopPath =
std::filesystem::path(repo->overlaySharedDir().absolutePath().toStdString()) / "applications"
/ "org.example.app.desktop";

std::filesystem::create_directories(layerEntriesDir);
std::filesystem::create_directories(overlayDesktopPath.parent_path());
std::ofstream(layerEntriesDir / "org.example.app.desktop") << "Test desktop content";
std::ofstream(overlayDesktopPath) << "Test desktop content in overlay";

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer,
printContent(ElementsAre(QString::fromStdString(overlayDesktopPath.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentMapsLegacySystemdUserPathToExportedLibPath)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-systemd-legacy";
const auto layerEntriesDir =
tempDir->path() / "layers" / commit / "entries" / "share" / "systemd" / "user";
const auto exportedDir = tempDir->path() / "entries" / "lib" / "systemd" / "user";
const auto exportedFile = exportedDir / "org.example.app.service";

std::filesystem::create_directories(layerEntriesDir);
std::filesystem::create_directories(exportedDir);
std::ofstream(layerEntriesDir / "org.example.app.service")
<< "[Service]\nExecStart=/bin/true\n";
std::ofstream(exportedFile) << "[Service]\nExecStart=/bin/true\n";

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer, printContent(ElementsAre(QString::fromStdString(exportedFile.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

TEST_F(CliTest, contentPrefersLibSystemdUserOverLegacySharePath)
{
auto ref = package::Reference::parse("main:org.example.app/1.0.0/x86_64");
ASSERT_TRUE(ref.has_value());

const std::string commit = "test-commit-systemd-lib";
const auto layerLibDir =
tempDir->path() / "layers" / commit / "entries" / "lib" / "systemd" / "user";
const auto layerLegacyDir =
tempDir->path() / "layers" / commit / "entries" / "share" / "systemd" / "user";
const auto exportedDir = tempDir->path() / "entries" / "lib" / "systemd" / "user";
const auto exportedFile = exportedDir / "org.example.app.service";

std::filesystem::create_directories(layerLibDir);
std::filesystem::create_directories(layerLegacyDir);
std::filesystem::create_directories(exportedDir);
std::ofstream(layerLibDir / "org.example.app.service") << "[Service]\nExecStart=/bin/true\n";
std::ofstream(layerLegacyDir / "org.example.app.service") << "[Service]\nExecStart=/bin/true\n";
std::ofstream(exportedFile) << "[Service]\nExecStart=/bin/true\n";

api::types::v1::RepositoryCacheLayersItem layerItem;
layerItem.commit = commit;
layerItem.info.kind = "app";

EXPECT_CALL(*repo, clearReference(_, _, _, _)).WillOnce(Return(*ref));
EXPECT_CALL(*repo, getLayerItem(_, _, _))
.WillRepeatedly(
[layerItem](const package::Reference &, std::string, const std::optional<std::string> &)
-> utils::error::Result<api::types::v1::RepositoryCacheLayersItem> {
return layerItem;
});
EXPECT_CALL(*printer, printContent(ElementsAre(QString::fromStdString(exportedFile.string()))))
.WillOnce(Return());

EXPECT_EQ(cli->content(cli::ContentOptions{ .appid = "org.example.app" }), 0);
}

} // namespace
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <gtest/gtest.h>

Check warning on line 4 in libs/linglong/tests/ll-tests/src/linglong/mocks/linglong_builder_mock.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <gtest/gtest.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include "common/tempdir.h"

Check warning on line 6 in libs/linglong/tests/ll-tests/src/linglong/mocks/linglong_builder_mock.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/builder/linglong_builder.h" not found.
#include "linglong/builder/linglong_builder.h"

Check warning on line 7 in libs/linglong/tests/ll-tests/src/linglong/mocks/linglong_builder_mock.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "ocppi/cli/crun/Crun.hpp" not found.
#include "ocppi/cli/crun/Crun.hpp"

#include <memory>

namespace linglong::builder {
class BuilderMock : public Builder
{
Expand All @@ -21,9 +24,8 @@
static linglong::repo::OSTreeRepo &initTempRepo()
{
auto tempRepoConfig = api::types::v1::RepoConfigV2{};
char tempPath[] = "/tmp/linglong-builder-test-XXXXXX";
std::string testDir = mkdtemp(tempPath);
static linglong::repo::OSTreeRepo repo(QDir(testDir.c_str()), tempRepoConfig);
static auto testDir = std::make_unique<TempDir>("linglong-builder-test-");
static linglong::repo::OSTreeRepo repo(QDir(testDir->path().c_str()), tempRepoConfig);
return repo;
}

Expand Down
Loading
Loading