mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
fix(assets): runtime asset lookup and document install/package layout
This commit is contained in:
@@ -116,6 +116,44 @@ meson compile -C build-debug
|
||||
```
|
||||
</details>
|
||||
|
||||
## Installation / Packaging
|
||||
|
||||
`meson install` now installs the binary and shipped assets separately using the normal prefix layout:
|
||||
|
||||
```text
|
||||
/usr/local/bin/noctalia
|
||||
/usr/local/share/noctalia/assets/...
|
||||
```
|
||||
|
||||
With a different Meson `prefix`/`datadir`, the same structure is preserved under that prefix.
|
||||
|
||||
For packagers, the important point is that Noctalia needs the `assets/` tree at runtime. Copying only the bare `noctalia` binary is not enough.
|
||||
|
||||
Portable bundle layouts are also supported:
|
||||
|
||||
```text
|
||||
bundle/
|
||||
noctalia
|
||||
assets/
|
||||
```
|
||||
|
||||
```text
|
||||
bundle/
|
||||
bin/noctalia
|
||||
share/noctalia/assets/
|
||||
```
|
||||
|
||||
Runtime asset lookup order:
|
||||
|
||||
1. `NOCTALIA_ASSETS_DIR`
|
||||
2. `assets/` next to the executable
|
||||
3. `assets/` one level above the executable
|
||||
4. install-style `../share/noctalia/assets` relative to the executable
|
||||
5. the compiled install path from Meson (`<prefix>/<datadir>/noctalia/assets`)
|
||||
6. the source-tree `assets/` directory as a development fallback
|
||||
|
||||
An asset root is only accepted if it contains the expected shipped files such as `emoji.json`, `fonts/tabler.ttf`, `templates/builtin.toml`, and `translations/en.json`.
|
||||
|
||||
## Code Style
|
||||
|
||||
This project uses [clang-format](https://clang.llvm.org/docs/ClangFormat.html) for formatting. Run `just format` before committing.
|
||||
|
||||
+9
-3
@@ -282,6 +282,7 @@ _noctalia_sources = files(
|
||||
'src/core/deferred_call.cpp',
|
||||
'src/core/log.cpp',
|
||||
'src/core/process.cpp',
|
||||
'src/core/resource_paths.cpp',
|
||||
'src/core/timer_manager.cpp',
|
||||
'src/core/ui_phase.cpp',
|
||||
'src/dbus/bluetooth/bluetooth_agent.cpp',
|
||||
@@ -530,9 +531,14 @@ executable('noctalia',
|
||||
include_directories: _noctalia_inc,
|
||||
cpp_args: [
|
||||
'-Wall', '-Wextra', '-Wpedantic', '-Wconversion', '-Wshadow',
|
||||
'-DNOCTALIA_ASSETS_DIR="' + meson.project_source_root() / 'assets' + '"',
|
||||
'-DNOCTALIA_I18N_DIR="' + meson.project_source_root() / 'assets' / 'translations' + '"',
|
||||
'-DNOCTALIA_SOURCE_ASSETS_DIR="' + meson.project_source_root() / 'assets' + '"',
|
||||
'-DNOCTALIA_INSTALL_PREFIX="' + get_option('prefix') + '"',
|
||||
'-DNOCTALIA_INSTALL_DATADIR="' + get_option('datadir') + '"',
|
||||
'-DNOCTALIA_VERSION="' + meson.project_version() + '"',
|
||||
],
|
||||
install: false,
|
||||
install: true,
|
||||
)
|
||||
|
||||
install_subdir('assets',
|
||||
install_dir: get_option('datadir') / 'noctalia',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "core/resource_paths.h"
|
||||
|
||||
#include "core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace paths {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("paths");
|
||||
|
||||
std::filesystem::path installedAssetsRoot() {
|
||||
const std::filesystem::path datadir(NOCTALIA_INSTALL_DATADIR);
|
||||
if (datadir.is_absolute()) {
|
||||
return datadir / "noctalia" / "assets";
|
||||
}
|
||||
return std::filesystem::path(NOCTALIA_INSTALL_PREFIX) / datadir / "noctalia" / "assets";
|
||||
}
|
||||
|
||||
std::filesystem::path sourceAssetsRoot() { return std::filesystem::path(NOCTALIA_SOURCE_ASSETS_DIR); }
|
||||
|
||||
bool isAssetRoot(const std::filesystem::path& root) {
|
||||
if (root.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(root / "emoji.json", ec) &&
|
||||
std::filesystem::exists(root / "fonts" / "tabler.ttf", ec) &&
|
||||
std::filesystem::exists(root / "templates" / "builtin.toml", ec) &&
|
||||
std::filesystem::exists(root / "translations" / "en.json", ec);
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> executablePath() {
|
||||
std::array<char, 4096> buffer{};
|
||||
const ssize_t count = ::readlink("/proc/self/exe", buffer.data(), buffer.size() - 1);
|
||||
if (count <= 0 || static_cast<std::size_t>(count) >= buffer.size() - 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
buffer[static_cast<std::size_t>(count)] = '\0';
|
||||
return std::filesystem::path(buffer.data());
|
||||
}
|
||||
|
||||
void appendUnique(std::vector<std::filesystem::path>& candidates, const std::filesystem::path& candidate) {
|
||||
if (candidate.empty()) {
|
||||
return;
|
||||
}
|
||||
if (std::find(candidates.begin(), candidates.end(), candidate) == candidates.end()) {
|
||||
candidates.push_back(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> assetCandidates() {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
|
||||
if (const char* env = std::getenv("NOCTALIA_ASSETS_DIR"); env != nullptr && env[0] != '\0') {
|
||||
const std::filesystem::path overridePath(env);
|
||||
if (isAssetRoot(overridePath)) {
|
||||
candidates.push_back(overridePath);
|
||||
return candidates;
|
||||
}
|
||||
kLog.warn("NOCTALIA_ASSETS_DIR is not a valid asset bundle: {}", overridePath.string());
|
||||
}
|
||||
|
||||
if (auto exe = executablePath()) {
|
||||
const std::filesystem::path exeDir = exe->parent_path();
|
||||
appendUnique(candidates, exeDir / "assets");
|
||||
appendUnique(candidates, exeDir.parent_path() / "assets");
|
||||
|
||||
const std::filesystem::path datadir(NOCTALIA_INSTALL_DATADIR);
|
||||
if (!datadir.empty() && !datadir.is_absolute()) {
|
||||
appendUnique(candidates, exeDir.parent_path() / datadir / "noctalia" / "assets");
|
||||
}
|
||||
appendUnique(candidates, exeDir.parent_path() / "share" / "noctalia" / "assets");
|
||||
}
|
||||
|
||||
appendUnique(candidates, installedAssetsRoot());
|
||||
appendUnique(candidates, sourceAssetsRoot());
|
||||
return candidates;
|
||||
}
|
||||
|
||||
std::filesystem::path resolveAssetsRoot() {
|
||||
for (const auto& candidate : assetCandidates()) {
|
||||
if (isAssetRoot(candidate)) {
|
||||
kLog.debug("using assets from {}", candidate.string());
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const std::filesystem::path fallback = installedAssetsRoot();
|
||||
kLog.warn("could not locate a valid asset bundle; defaulting to {}", fallback.string());
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::filesystem::path& assetsRoot() {
|
||||
static const std::filesystem::path s_root = resolveAssetsRoot();
|
||||
return s_root;
|
||||
}
|
||||
|
||||
std::filesystem::path assetPath(std::string_view relativePath) {
|
||||
return assetsRoot() / std::filesystem::path(std::string(relativePath));
|
||||
}
|
||||
|
||||
} // namespace paths
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
namespace paths {
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& assetsRoot();
|
||||
[[nodiscard]] std::filesystem::path assetPath(std::string_view relativePath);
|
||||
|
||||
} // namespace paths
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "i18n/i18n_service.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "core/resource_paths.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <json.hpp>
|
||||
#include <string>
|
||||
@@ -68,7 +70,7 @@ namespace i18n {
|
||||
}
|
||||
|
||||
bool Service::loadCatalog(std::string_view lang, Catalog& out) const {
|
||||
std::string path = std::string(NOCTALIA_I18N_DIR) + "/" + std::string(lang) + ".json";
|
||||
const std::filesystem::path path = paths::assetPath("translations/" + std::string(lang) + ".json");
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
@@ -76,7 +78,7 @@ namespace i18n {
|
||||
try {
|
||||
auto json = nlohmann::json::parse(file);
|
||||
if (!json.is_object()) {
|
||||
kLog.warn("catalog {} is not a JSON object", path);
|
||||
kLog.warn("catalog {} is not a JSON object", path.string());
|
||||
return false;
|
||||
}
|
||||
Catalog fresh;
|
||||
@@ -84,7 +86,7 @@ namespace i18n {
|
||||
out = std::move(fresh);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
kLog.error("failed to parse {}: {}", path, e.what());
|
||||
kLog.error("failed to parse {}: {}", path.string(), e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "launcher/emoji_provider.h"
|
||||
|
||||
#include "core/resource_paths.h"
|
||||
#include "wayland/clipboard_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <json.hpp>
|
||||
#include <sstream>
|
||||
@@ -20,7 +22,7 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
void EmojiProvider::initialize() {
|
||||
std::string path = std::string(NOCTALIA_ASSETS_DIR) + "/emoji.json";
|
||||
const std::filesystem::path path = paths::assetPath("emoji.json");
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "render/render_context.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "core/resource_paths.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/gl_shared_context.h"
|
||||
#include "render/render_target.h"
|
||||
@@ -73,7 +74,7 @@ void RenderContext::initialize(GlSharedContext& shared) {
|
||||
// Pango handles font fallback via Fontconfig automatically — no explicit chain.
|
||||
ensureGlPrograms();
|
||||
m_textRenderer.initialize(&m_glyphProgram);
|
||||
m_glyphRenderer.initialize(NOCTALIA_ASSETS_DIR "/fonts/tabler.ttf", &m_glyphProgram);
|
||||
m_glyphRenderer.initialize(paths::assetPath("fonts/tabler.ttf").string(), &m_glyphProgram);
|
||||
}
|
||||
|
||||
void RenderContext::ensureGlPrograms() {
|
||||
|
||||
+6
-3
@@ -1,5 +1,6 @@
|
||||
#include "theme/cli.h"
|
||||
|
||||
#include "core/resource_paths.h"
|
||||
#include "core/toml.h"
|
||||
#include "theme/color.h"
|
||||
#include "theme/fixed_palette.h"
|
||||
@@ -53,7 +54,7 @@ namespace noctalia::theme {
|
||||
" --list-builtins List built-in templates from the shipped catalog\n"
|
||||
" --default-mode Template default mode: dark or light";
|
||||
|
||||
constexpr const char* kBuiltinTemplateConfig = NOCTALIA_ASSETS_DIR "/templates/builtin.toml";
|
||||
std::filesystem::path builtinTemplateConfigPath() { return paths::assetPath("templates/builtin.toml"); }
|
||||
|
||||
struct BuiltinTemplateInfo {
|
||||
std::string id;
|
||||
@@ -77,7 +78,7 @@ namespace noctalia::theme {
|
||||
std::vector<BuiltinTemplateInfo> loadBuiltinTemplateInfo(std::string& err) {
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(kBuiltinTemplateConfig);
|
||||
root = toml::parse_file(builtinTemplateConfigPath().string());
|
||||
} catch (const toml::parse_error& e) {
|
||||
err = e.description();
|
||||
return {};
|
||||
@@ -272,6 +273,7 @@ namespace noctalia::theme {
|
||||
Variant variant = Variant::Dark;
|
||||
const char* outPath = nullptr;
|
||||
const char* configPath = nullptr;
|
||||
std::string builtinConfigPathStorage;
|
||||
bool builtinConfig = false;
|
||||
bool listBuiltins = false;
|
||||
std::string defaultMode = "dark";
|
||||
@@ -356,7 +358,8 @@ namespace noctalia::theme {
|
||||
std::fputs("error: --builtin-config cannot be combined with --config\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
configPath = kBuiltinTemplateConfig;
|
||||
builtinConfigPathStorage = builtinTemplateConfigPath().string();
|
||||
configPath = builtinConfigPathStorage.c_str();
|
||||
}
|
||||
|
||||
if (!imagePath && !themeJsonPath) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "core/log.h"
|
||||
#include "core/resource_paths.h"
|
||||
#include "theme/template_engine.h"
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -13,7 +14,8 @@ namespace noctalia::theme {
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("theme_templates");
|
||||
constexpr const char* kBuiltinTemplateConfig = NOCTALIA_ASSETS_DIR "/templates/builtin.toml";
|
||||
|
||||
std::filesystem::path builtinTemplateConfigPath() { return paths::assetPath("templates/builtin.toml"); }
|
||||
|
||||
std::filesystem::path expandUserPath(const std::string& path) {
|
||||
if (path.empty() || path[0] != '~')
|
||||
@@ -53,8 +55,9 @@ namespace noctalia::theme {
|
||||
TemplateEngine::Options builtinOptions = options;
|
||||
builtinOptions.enabledTemplates.insert(templateCfg.builtinIds.begin(), templateCfg.builtinIds.end());
|
||||
TemplateEngine builtinEngine(TemplateEngine::makeThemeData(palette), std::move(builtinOptions));
|
||||
if (!builtinEngine.processConfigFile(kBuiltinTemplateConfig)) {
|
||||
kLog.warn("failed to apply built-in templates from {}", kBuiltinTemplateConfig);
|
||||
const std::filesystem::path builtinConfig = builtinTemplateConfigPath();
|
||||
if (!builtinEngine.processConfigFile(builtinConfig)) {
|
||||
kLog.warn("failed to apply built-in templates from {}", builtinConfig.string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user