-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathappdir_root_setup.cpp
More file actions
403 lines (321 loc) · 19.4 KB
/
appdir_root_setup.cpp
File metadata and controls
403 lines (321 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// system headers
#include <fstream>
// local headers
#include <linuxdeploy/util/util.h>
#include <linuxdeploy/log/log.h>
#include "appdir_root_setup.h"
namespace fs = std::filesystem;
namespace linuxdeploy {
using namespace desktopfile;
namespace core {
using namespace appdir;
using namespace log;
class AppDirRootSetup::Private {
public:
static constexpr auto APPRUN_HOOKS_DIRNAME = "apprun-hooks";
public:
const AppDir& appDir;
public:
explicit Private(const AppDir& appDir) : appDir(appDir) {}
public:
static void makeFileExecutable(const fs::path& path) {
fs::permissions(path,
fs::perms::owner_all | fs::perms::group_read | fs::perms::others_read |
fs::perms::group_exec | fs::perms::others_exec
);
}
public:
static int getIconPreference(const fs::path &iconPath) {
int iconWidth = 0;
try {
// Assuming that iconPath ends with WxH[@D]/apps/*.*, pick the W component, taking
// the D component into account if it's there
const auto dirName = iconPath.parent_path().parent_path().filename().string();
const auto dpiPos = dirName.rfind('@');
iconWidth = std::stoi(dirName)
* (dpiPos != std::string::npos ? std::stoi(dirName.substr(dpiPos + 1)) : 1);
} catch (const std::logic_error &) {
ldLog() << LD_WARNING << "Icon size of" << iconPath << "could not be determined" << std::endl;
// size remains zero
}
// Preference takes values from 0 to 100, with the highest value reached for 64x64 icons, going down
// as width goes from 64 either way. For "equally remote" sizes (32x32 and 128x128, e.g.) the larger
// icons are preferred (preference value for 32x32 is 49 while 128x128 are at 50).
// For other examples, 96x96 yields 66, 48x48 gets 73, and so on.
// Also, since size cannot be determined for icons in pixmaps/, preference for these is 0; in other
// words, icons in /usr/share/icons/hicolor are preferred, in alignment with the icon naming spec
return iconWidth < 64 ? 100 * iconWidth / 65 : 6400 / iconWidth;
}
public:
bool deployDesktopFileAndIcon(const DesktopFile& desktopFile) const {
ldLog() << "Deploying desktop file to AppDir root:" << desktopFile.path() << std::endl;
// copy desktop file to root directory
if (!appDir.createRelativeSymlink(desktopFile.path(), appDir.path())) {
ldLog() << LD_ERROR << "Failed to create link to desktop file in AppDir root:" << desktopFile.path() << std::endl;
return false;
}
// look for suitable icon
DesktopFileEntry iconEntry;
if (!desktopFile.getEntry("Desktop Entry", "Icon", iconEntry) || iconEntry.value().empty()) {
ldLog() << LD_ERROR << "Icon entry missing in desktop file:" << desktopFile.path() << std::endl;
return false;
}
const auto iconName = iconEntry.value();
auto foundIconPaths = appDir.deployedIconPaths();
if (foundIconPaths.empty()) {
ldLog() << LD_ERROR << "Could not find icon executable for Icon entry:" << iconEntry.value() << std::endl;
return false;
}
// There's no way of knowing the target environment where an AppImage icon will be shown
// therefore:
// - SVG is prefered over raster icons
// - 64x64 is picked as a reasonable best size for raster icons;
// the farther the icon dimensions are from that, the less preferred the icon is
auto bestIcon = foundIconPaths.end();
for (auto iconPath = foundIconPaths.begin(); iconPath != foundIconPaths.end(); ++iconPath) {
if (iconPath->string().size() < iconName.size())
continue; // No chance to match anything
if (iconName.front() == '/') { // Full path to the icon specified
const auto iconPathStr = iconPath->string();
// Check if the current icon path ends with the path specified in the desktop entry
// (strictly speaking, it should also start with $DESTDIR; this is left for another time)
if (std::equal(iconName.rbegin(), iconName.rend(), iconPathStr.rbegin())) {
bestIcon = iconPath;
break;
}
continue;
}
const bool matchesFilenameWithExtension = iconPath->filename() == iconName;
if (iconPath->stem() != iconEntry.value() && !matchesFilenameWithExtension)
continue;
ldLog() << LD_DEBUG << "Icon found:" << *iconPath << std::endl;
if (matchesFilenameWithExtension) {
ldLog() << LD_WARNING << "Icon= entry filename contains extension" << std::endl;
}
if (bestIcon == foundIconPaths.end()) {
bestIcon = iconPath;
continue;
}
// From here, the code comparing the current icon and the so far best icon begins
const auto currentExtension = util::strLower(iconPath->extension().string());
const auto bestIconExtension = util::strLower(bestIcon->extension().string());
// SVGs are preferred, and (normally) only come in scalable/apps/; process them early
if (currentExtension == ".svg") {
// There's only one spec-compliant place for an SVG icon (icons/scalable/apps); but if
// a full filename is used in the desktop file (Icon=a.svg) then two SVG icons can
// match it: scalable/apps/a.svg and scalable/apps/a.svg.svg; in this case a.svg wins
if (matchesFilenameWithExtension || bestIconExtension != ".svg")
bestIcon = iconPath;
break; // Further icons can't be better than what bestIcon has now.
}
// As of here, the _current_ icon is a raster one (PNG or XPM)
if (bestIconExtension == ".svg" // SVG is always better
|| (!matchesFilenameWithExtension && bestIcon->filename() == iconName)) // Better filename match
continue;
// Both icons are raster
if (matchesFilenameWithExtension && bestIcon->filename() != iconName) { // The other way around
bestIcon = iconPath;
continue;
}
// Get preferences (declared) sizes of the icons from the directory name and compare.
//
// The code diverts from Icon Naming Spec here, since the spec relies on reading
// index.theme data and taking Threshold and MinSize/MaxSize values from there. Instead,
// merely figure which size is "logarithmically further" from the sweet spot of 64,
// preferring larger icons in case of a tie (see getIconPreference() implementation);
// as a last resort, if the preference is the same (e.g. two icons deployed to pixmaps/),
// PNGs win over XPMs.
const auto currentPreference = getIconPreference(*iconPath);
const auto bestPreference = getIconPreference(*bestIcon);
if (currentPreference > bestPreference
|| (currentPreference == bestPreference && currentExtension < bestIconExtension))
bestIcon = iconPath;
}
if (bestIcon == foundIconPaths.end()) {
ldLog() << LD_ERROR << "Could not find suitable icon for Icon entry:" << iconEntry.value() << std::endl;
return false;
}
ldLog() << "Deploying icon to AppDir root:" << *bestIcon << std::endl;
if (!appDir.createRelativeSymlink(*bestIcon, appDir.path())) {
ldLog() << LD_ERROR << "Failed to create symlink for icon in AppDir root:" << *bestIcon << std::endl;
return false;
}
return true;
}
bool deployCustomAppRunFile(const fs::path& customAppRunPath) const {
// copy custom AppRun executable
ldLog() << "Deploying custom AppRun:" << customAppRunPath << std::endl;
const auto appRunPath = appDir.path() / "AppRun";
if (!appDir.copyFile(customAppRunPath, appRunPath))
return false;
ldLog() << "Making AppRun file executable: " << appRunPath << std::endl;
makeFileExecutable(appRunPath);
return true;
}
bool deployStandardAppRunFromDesktopFile(const DesktopFile& desktopFile, const fs::path& customAppRunPath) const {
const fs::path appRunPath(appDir.path() / "AppRun");
// check if there is a custom AppRun already
// in that case, skip deployment of symlink
if (fs::exists(appRunPath)) {
ldLog() << LD_WARNING << "Existing AppRun detected, skipping deployment of symlink" << std::endl;
} else {
// look for suitable binary to create AppRun symlink
DesktopFileEntry executableEntry;
if (!desktopFile.getEntry("Desktop Entry", "Exec", executableEntry)) {
ldLog() << LD_ERROR << "Exec entry missing in desktop file:" << desktopFile.path()
<< std::endl;
return false;
}
auto executableName = util::split(executableEntry.value())[0];
const auto foundExecutablePaths = appDir.deployedExecutablePaths();
if (foundExecutablePaths.empty()) {
ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << executableName
<< std::endl;
return false;
}
bool deployedExecutable = false;
for (const auto& executablePath : foundExecutablePaths) {
ldLog() << LD_DEBUG << "Executable found:" << executablePath << std::endl;
if (executablePath.filename() == executableName) {
ldLog() << "Deploying AppRun symlink for executable in AppDir root:" << executablePath
<< std::endl;
if (!appDir.createRelativeSymlink(executablePath, appRunPath)) {
ldLog() << LD_ERROR
<< "Failed to create AppRun symlink for executable in AppDir root:"
<< executablePath << std::endl;
return false;
}
deployedExecutable = true;
break;
}
}
if (!deployedExecutable) {
ldLog() << LD_ERROR << "Could not deploy symlink for executable: could not find suitable executable for Exec entry:" << executableName << std::endl;
return false;
}
}
if (!fs::exists(appRunPath)) {
ldLog() << LD_ERROR << "AppRun deployment failed unexpectedly" << std::endl;
return false;
}
// as a convenience feature, we just make the deployed AppRun file executable, so the user won't be
// surprised during runtime when they forgot to do so
// this is done for custom AppRun files, too
if (fs::is_symlink(appRunPath)) {
ldLog() << LD_DEBUG << "Deployed AppRun is a symlink, not making executable" << std::endl;
} else {
ldLog() << LD_DEBUG << "Deployed AppRun is not a symlink, making executable" << std::endl;
makeFileExecutable(appRunPath);
}
return true;
}
bool deployAppRunWrapperIfNecessary() const {
const fs::path appRunPath(appDir.path() / "AppRun");
const fs::path wrappedAppRunPath(appRunPath.string() + ".wrapped");
const fs::path appRunHooksPath(appDir.path() / APPRUN_HOOKS_DIRNAME);
// first, we check whether there's that special directory containing hooks
if (!fs::is_directory(appRunHooksPath)) {
ldLog() << LD_DEBUG << "Could not find apprun-hooks dir, no need to deploy the AppRun wrapper" << std::endl;
return true;
}
std::vector<fs::path> fileList;
std::copy_if(
fs::directory_iterator(appRunHooksPath), fs::directory_iterator {},
std::back_inserter(fileList),
[](const fs::path& path) {
return fs::is_regular_file(path);
});
// if there's no files in there we don't have to do anything
if (fileList.empty()) {
ldLog() << LD_WARNING << "Found an empty apprun-hooks directory, assuming there is no need to deploy the AppRun wrapper" << std::endl;
return true;
}
// sort the file list so that the order of execution is deterministic
std::sort(fileList.begin(), fileList.end());
// any file within that directory is considered to be a script
// we can't perform any validity checks, that would be way too much complexity and even tools which
// claim they can, like e.g., shellcheck, aren't perfect, they only aid in avoiding bugs but cannot
// prevent them completely
// let's put together the wrapper script's contents
std::ostringstream oss;
oss << "#! /usr/bin/env bash" << std::endl
<< std::endl
<< "# autogenerated by linuxdeploy" << std::endl
<< std::endl
<< "# make sure errors in sourced scripts will cause this script to stop" << std::endl
<< "set -e" << std::endl
<< std::endl
<< "this_dir=\"$(readlink -f \"$(dirname \"$0\")\")\"" << std::endl
<< std::endl;
std::for_each(fileList.begin(), fileList.end(), [&oss](const fs::path& p) {
oss << "source \"$this_dir\"/" << APPRUN_HOOKS_DIRNAME << "/" << p.filename() << std::endl;
});
oss << std::endl
<< "exec \"$this_dir\"/AppRun.wrapped \"$@\"" << std::endl;
// first we need to make sure we're not running this more than once
// this might cause more harm than good
// we require the user to clean up the mess at first
// FIXME: try to find a way how to rewrap AppRun on subsequent runs or, even better, become idempotent
if (fs::exists(wrappedAppRunPath)) {
ldLog() << LD_WARNING << "Already found wrapped AppRun, using existing file/symlink" << std::endl;
} else {
// backup original AppRun
fs::rename(appRunPath, wrappedAppRunPath);
}
// in case the above check triggered a warning, it's possible that there is another AppRun in the AppDir
// this one has to be cleaned up in that case
if (fs::exists(appRunPath)) {
ldLog() << LD_WARNING << "Found an AppRun file/symlink, possibly due to re-run of linuxdeploy, "
"overwriting" << std::endl;
fs::remove(appRunPath);
}
// install new script
std::ofstream ofs(appRunPath.string());
ofs << oss.str();
// make sure data is written to disk
ofs.flush();
ofs.close();
// make new file executable
makeFileExecutable(appRunPath);
// we're done!
return true;
}
};
AppDirRootSetup::AppDirRootSetup(const AppDir& appDir) : d(new Private(appDir)) {}
bool AppDirRootSetup::run(const DesktopFile& desktopFile, const fs::path& customAppRunPath) const {
// first step that is always required is to deploy the desktop file and the corresponding icon
if (!d->deployDesktopFileAndIcon(desktopFile)) {
ldLog() << LD_DEBUG << "deployDesktopFileAndIcon returned false" << std::endl;
return false;
}
// the algorithm depends on whether the user wishes to deploy their own AppRun file
// in case they do, the algorithm shall deploy that file
// otherwise, the standard algorithm shall be run which takes information from the desktop file to
// deploy a symlink pointing to the AppImage's main binary
// this allows power users to define their own AppImage initialization steps or run different binaries
// based on parameters etc.
if (!customAppRunPath.empty()) {
if (!d->deployCustomAppRunFile(customAppRunPath)) {
ldLog() << LD_DEBUG << "deployCustomAppRunFile returned false" << std::endl;
return false;
}
} else {
if (!d->deployStandardAppRunFromDesktopFile(desktopFile, customAppRunPath)) {
ldLog() << LD_DEBUG << "deployStandardAppRunFromDesktopFile returned false" << std::endl;
return false;
}
}
// plugins might need to run some initializing code to make certain features work
// these involve setting environment variables because libraries or frameworks don't support any other
// way of pointing them to resources inside the AppDir instead of looking into config files in locations
// inside the AppImage, etc.
// the linuxdeploy plugin specification states that if plugins put files into a specified directory in
// the AppImage, linuxdeploy will make sure they're run before running the regular AppRun
if (!d->deployAppRunWrapperIfNecessary()) {
ldLog() << LD_DEBUG << "deployAppRunWrapperIfNecessary returned false" << std::endl;
return false;
}
return true;
}
}
}