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
2 changes: 1 addition & 1 deletion configure.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
// The directory where the package install hooks reside.
#define LINGLONG_INSTALL_HOOKS_DIR LINGLONG_SYSCONFDIR "/config.d"

#define LINGLONG_EXPORT_VERSION "1.0.0.4"
#define LINGLONG_EXPORT_VERSION "1.0.0.5"
// The package's locale domain.
#define PACKAGE_LOCALE_DOMAIN "@GETTEXT_DOMAIN_NAME@"

Expand Down
261 changes: 257 additions & 4 deletions libs/linglong/src/linglong/repo/ostree_repo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
#include "linglong/api/types/v1/ExportDirs.hpp"
#include "linglong/api/types/v1/Generators.hpp" // IWYU pragma: keep
#include "linglong/api/types/v1/PackageInfoV2.hpp"
#include "linglong/api/types/v1/Repo.hpp"

Check warning on line 13 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/PackageInfoV2.hpp" not found.
#include "linglong/api/types/v1/RepositoryCacheLayersItem.hpp"

Check warning on line 14 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/Repo.hpp" not found.
#include "linglong/api/types/v1/RepositoryCacheMergedItem.hpp"

Check warning on line 15 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/RepositoryCacheLayersItem.hpp" not found.
#include "linglong/common/error.h"

Check warning on line 16 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/RepositoryCacheMergedItem.hpp" not found.
#include "linglong/common/formatter.h"

Check warning on line 17 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/common/formatter.h" not found.
#include "linglong/common/gkeyfile_wrapper.h"

Check warning on line 18 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/common/gkeyfile_wrapper.h" not found.
#include "linglong/common/strings.h"

Check warning on line 19 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/common/strings.h" not found.
#include "linglong/package/fuzzy_reference.h"
#include "linglong/package/layer_dir.h"
#include "linglong/package/reference.h"
Expand Down Expand Up @@ -45,15 +46,17 @@
#include <QProcess>
#include <QTemporaryDir>
#include <QTimer>
#include <QtGlobal>

Check warning on line 49 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 50 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 52 in libs/linglong/src/linglong/repo/ostree_repo.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <algorithm> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <optional>
Expand All @@ -66,6 +69,7 @@
#include <vector>

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

namespace linglong::repo {
Expand Down Expand Up @@ -1771,7 +1775,9 @@
// specify a directory. these files do not belong to the applications and should not be
// printed in the log.
if (info.absoluteFilePath().contains("share/mime")
|| info.absoluteFilePath().contains("share/applications")) {
|| info.absoluteFilePath().contains("share/applications")
|| info.absoluteFilePath().startsWith(entriesDir.absoluteFilePath("bin") + "/")) {
// entries/bin holds regular wrapper script files that are not symlinks.
continue;
}

Expand Down Expand Up @@ -1803,10 +1809,68 @@
}
}

// Clean up binary wrapper scripts in entries/bin at the appID level: the wrapper is
// removed only when no other undeleted layer with the same app id remains outside this
// unexport batch. This keeps the wrapper alive across upgrades and module-level
// uninstalls and removes it when the last layer of the app is uninstalled.
std::vector<std::string> handledAppIDs;
for (const auto &layerPath : layerDirs) {
package::LayerDir layerDir(layerPath);
auto infoResult = layerDir.info();
if (!infoResult) {
LogW("Failed to read info from {}: {}",
layerPath.string(),
infoResult.error().message());
continue;
}

auto appID = infoResult->id;
if (std::find(handledAppIDs.cbegin(), handledAppIDs.cend(), appID)
!= handledAppIDs.cend()) {
continue;
}
handledAppIDs.emplace_back(appID);

// Is there still an undeleted layer with the same app id that is not part of this
// unexport batch?
auto remaining =
this->cache->queryLayerItem(repoCacheQuery{ .id = appID, .deleted = false });
auto hasOtherLayer =
std::any_of(remaining.cbegin(), remaining.cend(), [&](const auto &item) {
auto itemPath = this->layerPath(item.commit).lexically_normal();
return std::none_of(layerDirs.cbegin(), layerDirs.cend(), [&](const auto &p) {
return p.lexically_normal() == itemPath;
});
});

auto binDir = rootEntriesDir / "bin";
if (hasOtherLayer) {
// Upgrade or module-level uninstall: another version/module of the app remains.
LogI("Keep binary wrapper for {}: another layer still exists", appID);
continue;
}

// The last layer of this app is being removed: delete the wrapper contents.
std::error_code ec;
std::filesystem::remove(binDir / appID, ec);
if (ec) {
LogW("Failed to remove binary wrapper {} for {}: {}",
(binDir / appID).string(),
appID,
ec.message());
}
auto appBinDir = binDir / "apps" / appID;
std::filesystem::remove_all(appBinDir, ec);
if (ec) {
LogW("Failed to remove {}: {}", appBinDir.string(), ec.message());
}
}

this->updateSharedInfo();

const QString entriesRoot = entriesDir.absolutePath();
std::function<void(const QString &path)> removeEmptySubdirectories =
[&removeEmptySubdirectories](const QString &path) {
[&removeEmptySubdirectories, &entriesRoot](const QString &path) {
QDir dir(path);

// 获取目录下的所有子目录
Expand All @@ -1822,15 +1886,20 @@
// 检查子目录是否为空
QDir subDir(subDirPath);
if (subDir.isEmpty()) {
// Keep the top-level bin/share directories (referenced by the
// PATH/XDG_DATA_DIRS injection, created by the export logic) so the injected
// environment stays stable across uninstalls.
bool topLevelBinShare = path == entriesRoot
&& (entry.fileName() == "bin" || entry.fileName() == "share");
// 如果子目录为空,则删除它
if (!subDir.rmdir(subDirPath)) {
if (!topLevelBinShare && !subDir.rmdir(subDirPath)) {
LogD("Failed to remove directory: {}", subDirPath.toStdString());
continue;
}
}
}
};
removeEmptySubdirectories(entriesDir.absolutePath());
removeEmptySubdirectories(entriesRoot);

return LINGLONG_OK;
}
Expand Down Expand Up @@ -2161,6 +2230,14 @@
{
LINGLONG_TRACE(fmt::format("export app entries for {}", item.info.id));

// Binary wrapper scripts are exported regardless of the white-listed app entries:
// the appid-named wrapper is generated even when the layer has no entries directory
// or the host lacks the export-dirs.json whitelist, so the script is always created.
auto binRet = this->exportAppBinaries(rootEntriesDir, item);
if (!binRet) {
return binRet;
}

auto layerDir = getLayerDir(item);
if (!layerDir.has_value()) {
return LINGLONG_ERR("get layer dir", layerDir);
Expand Down Expand Up @@ -2234,6 +2311,182 @@
return ret;
}
}

return LINGLONG_OK;
}

namespace {

// Build the script content for a wrapper script: each element of command is
// single-quoted for shell safety.
std::string buildWrapperScriptContent(const std::string &appID,
const std::vector<std::string> &command)
{
auto quotedAppID = common::strings::quoteBashArg(appID);

std::string cmdArgs;
for (const auto &arg : command) {
if (!cmdArgs.empty()) {
cmdArgs += ' ';
}
cmdArgs += common::strings::quoteBashArg(arg);
}
return fmt::format("#!/usr/bin/env sh\nexec ll-cli run {} -- {} \"$@\"\n",
quotedAppID,
cmdArgs);
}

// Content: #!/usr/bin/env sh\nexec ll-cli run <appID> -- <command...> "$@"
// The wrapper is created atomically with O_EXCL. If the file already exists (e.g. a
// previous version was installed), the creation is idempotent: identical content is a
// no-op and different content is overwritten, so the script always tracks the latest command.
utils::error::Result<void>
createBinaryWrapperScript(const std::filesystem::path &path,
const std::string &appID,
const std::vector<std::string> &command) noexcept
{
LINGLONG_TRACE(fmt::format("create binary wrapper script {}", path.string()));

auto dir = path.parent_path();
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
return LINGLONG_ERR(
fmt::format("create directories for {}: {}", path.string(), ec.message()));
}

const std::string content = buildWrapperScriptContent(appID, command);

int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0755);
if (fd < 0) {
if (errno != EEXIST) {
return LINGLONG_ERR(
fmt::format("open {}: {}", path.string(), common::error::errorString(errno)));
}

// The wrapper already exists. Keep it untouched when the content matches, otherwise
// overwrite so the exported command always reflects the latest version.
auto existing = linglong::utils::readFile(path);
if (existing && *existing == content) {
return LINGLONG_OK;
}
fd = ::open(path.c_str(), O_WRONLY | O_TRUNC | O_CLOEXEC);
if (fd < 0) {
return LINGLONG_ERR(fmt::format("open {} for overwrite: {}",
path.string(),
common::error::errorString(errno)));
}
}

// fchmod ensures executable bit regardless of process umask
if (::fchmod(fd, 0755) != 0) {
::close(fd);
std::error_code rmEc;
std::filesystem::remove(path, rmEc);
return LINGLONG_ERR(
fmt::format("fchmod {}: {}", path.string(), common::error::errorString(errno)));
}

ssize_t written = ::write(fd, content.data(), content.size());
if (written < 0) {
::close(fd);
std::error_code rmEc;
std::filesystem::remove(path, rmEc);
return LINGLONG_ERR(
fmt::format("write to {}: {}", path.string(), common::error::errorString(errno)));
}
if (static_cast<size_t>(written) != content.size()) {
::close(fd);
std::error_code rmEc;
std::filesystem::remove(path, rmEc);
return LINGLONG_ERR(fmt::format("partial write to {}", path.string()));
}

if (::close(fd) != 0) {
std::error_code rmEc;
std::filesystem::remove(path, rmEc);
return LINGLONG_ERR(
fmt::format("close {}: {}", path.string(), common::error::errorString(errno)));
}

return LINGLONG_OK;
}
} // namespace

utils::error::Result<void>
OSTreeRepo::exportAppBinaries(const std::filesystem::path &rootEntriesDir,
const api::types::v1::RepositoryCacheLayersItem &item) noexcept
{
LINGLONG_TRACE(fmt::format("export app binaries for {}", item.info.id));

// The appid-named wrapper is owned by the binary module; other modules (e.g.
// develop) must not overwrite it.
if (item.info.packageInfoV2Module != "binary") {
return LINGLONG_OK;
}

// command is required for app kind; guard defensively
const auto &cmd = item.info.command.value_or(std::vector<std::string>{});
if (cmd.empty()) {
// A previous version may have exported a wrapper script. Drop it so entries/bin
// matches the current (command-less) layer, consistent with the cleanup that
// unexportAppEntries performs.
LogW("Skipping binary export for {}: command field is empty; removing stale wrapper",
item.info.id);
auto staleBinDir = rootEntriesDir / "bin";
auto staleAppBinDir = staleBinDir / "apps" / item.info.id;
std::error_code staleEc;
std::filesystem::remove(staleBinDir / item.info.id, staleEc);
std::filesystem::remove_all(staleAppBinDir, staleEc);
return LINGLONG_OK;
}

auto binDir = rootEntriesDir / "bin";
// Actual scripts live under entries/bin/apps/APPID/bin/
auto appBinDir = binDir / "apps" / item.info.id / "bin";

// The entries/bin and entries/share dirs are not pre-created by tmpfiles
// anymore; the wrapper script path below creates entries/bin/apps/<APPID>/bin,
// while entries/share (referenced by the environment injection) is ensured here.
std::error_code dirEc;
std::filesystem::create_directories(rootEntriesDir / "share", dirEc);
if (dirEc) {
return LINGLONG_ERR(
fmt::format("create {}: {}", (rootEntriesDir / "share").string(), dirEc.message()));
}

// Helper: create a wrapper script and a symlink in entries/bin/ pointing to it
auto exportOne = [&](const std::string &name, const std::vector<std::string> &command) -> void {
auto realScript = appBinDir / name;
auto ret = createBinaryWrapperScript(realScript, item.info.id, command);
if (!ret) {
LogW("Failed to create binary script {} for {}: {}",
name,
item.info.id,
ret.error().message());
return;
}
// Create relative symlink: entries/bin/<name> → apps/APPID/bin/<name>
auto linkPath = binDir / name;
auto target = std::filesystem::path("apps") / item.info.id / "bin" / name;
std::error_code ec;
if (std::filesystem::exists(linkPath, ec) || std::filesystem::is_symlink(linkPath, ec)) {
// Already exists (e.g. upgrade); remove old link before creating new one
std::filesystem::remove(linkPath, ec);
}
std::filesystem::create_symlink(target, linkPath, ec);
if (ec) {
LogW("Failed to create symlink {} → {} for {}: {}",
linkPath.string(),
target.string(),
item.info.id,
ec.message());
}
};

// Always export a single script named after the appid, using the full command array
exportOne(item.info.id, cmd);

return LINGLONG_OK;
}

Expand Down
4 changes: 4 additions & 0 deletions libs/linglong/src/linglong/repo/ostree_repo.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ class OSTreeRepo : public QObject
virtual utils::error::Result<bool> shouldExportSignData() const noexcept;
utils::error::Result<void> exportAppEntries(
const std::filesystem::path &, const api::types::v1::RepositoryCacheLayersItem &) noexcept;
// Export a single wrapper script named after the appid to entries/bin
utils::error::Result<void>
exportAppBinaries(const std::filesystem::path &rootEntriesDir,
const api::types::v1::RepositoryCacheLayersItem &item) noexcept;
utils::error::Result<void> unexportLayerSignData(
const std::filesystem::path &, const api::types::v1::RepositoryCacheLayersItem &) noexcept;
utils::error::Result<void> unexportAppEntries(
Expand Down
12 changes: 12 additions & 0 deletions libs/linglong/tests/ll-tests/src/linglong/mocks/ostree_repo_mock.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ class MockOstreeRepo : public repo::OSTreeRepo
return this->OSTreeRepo::exportAppEntries(destination, item);
}

utils::error::Result<void>
exportAppBinaries(const std::filesystem::path &destination,
const api::types::v1::RepositoryCacheLayersItem &item)
{
return this->OSTreeRepo::exportAppBinaries(destination, item);
}

utils::error::Result<void>
exportLayerSignData(const std::filesystem::path &destination,
const api::types::v1::RepositoryCacheLayersItem &item)
Expand All @@ -58,6 +65,11 @@ class MockOstreeRepo : public repo::OSTreeRepo
return this->OSTreeRepo::unexportAppEntries(destination, layerDirs);
}

utils::error::Result<void> initCache(bool create = false)
{
return this->OSTreeRepo::initCache(create);
}

// mock getOverlayShareDir
std::function<std::filesystem::path()> wrapGetOverlayShareDirFunc;
std::function<utils::error::Result<bool>()> wrapShouldExportSignDataFunc;
Expand Down
Loading
Loading