BSD 14 mods - move to ffmpeg6, move to python39, fix OpenEXR build; add missing inclu...
[goodguy/cinelerra.git] / cinelerra-5.1 / tools / makeappimagetool / appdir_root_setup.cpp
1 // local headers
2 #include <fstream>
3 #include "includes/util.h"
4 #include "includes/log.h"
5 #include "includes/appdir_root_setup.h"
6
7
8 namespace bf = boost::filesystem;
9
10 namespace linuxdeploy {
11     using namespace desktopfile;
12
13     namespace core {
14         using namespace appdir;
15         using namespace log;
16
17
18         class AppDirRootSetup::Private {
19         public:
20             static constexpr auto APPRUN_HOOKS_DIRNAME = "apprun-hooks";
21
22         public:
23             const AppDir& appDir;
24
25         public:
26             explicit Private(const AppDir& appDir) : appDir(appDir) {}
27
28         public:
29             static void makeFileExecutable(const bf::path& path) {
30                 bf::permissions(path,
31                     bf::perms::owner_all | bf::perms::group_read | bf::perms::others_read |
32                     bf::perms::group_exe | bf::perms::others_exe
33                 );
34             }
35
36         public:
37             bool deployDesktopFileAndIcon(const DesktopFile& desktopFile) const {
38                 ldLog() << "Deploying desktop file to AppDir root:" << desktopFile.path() << std::endl;
39
40                 // copy desktop file to root directory
41                 if (!appDir.createRelativeSymlink(desktopFile.path(), appDir.path())) {
42                     ldLog() << LD_ERROR << "Failed to create link to desktop file in AppDir root:" << desktopFile.path() << std::endl;
43                     return false;
44                 }
45
46                 // look for suitable icon
47                 DesktopFileEntry iconEntry;
48
49                 if (!desktopFile.getEntry("Desktop Entry", "Icon", iconEntry)) {
50                     ldLog() << LD_ERROR << "Icon entry missing in desktop file:" << desktopFile.path() << std::endl;
51                     return false;
52                 }
53
54                 bool iconDeployed = false;
55
56                 const auto foundIconPaths = appDir.deployedIconPaths();
57
58                 if (foundIconPaths.empty()) {
59                     ldLog() << LD_ERROR << "Could not find icon executable for Icon entry:" << iconEntry.value() << std::endl;
60                     return false;
61                 }
62
63                 for (const auto& iconPath : foundIconPaths) {
64                     ldLog() << LD_DEBUG << "Icon found:" << iconPath << std::endl;
65
66                     const bool matchesFilenameWithExtension = iconPath.filename() == iconEntry.value();
67
68                     if (iconPath.stem() == iconEntry.value() || matchesFilenameWithExtension) {
69                         if (matchesFilenameWithExtension) {
70                             ldLog() << LD_WARNING << "Icon= entry filename contains extension" << std::endl;
71                         }
72
73                         ldLog() << "Deploying icon to AppDir root:" << iconPath << std::endl;
74
75                         if (!appDir.createRelativeSymlink(iconPath, appDir.path())) {
76                             ldLog() << LD_ERROR << "Failed to create symlink for icon in AppDir root:" << iconPath << std::endl;
77                             return false;
78                         }
79
80                         iconDeployed = true;
81                         break;
82                     }
83                 }
84
85                 if (!iconDeployed) {
86                     ldLog() << LD_ERROR << "Could not find suitable icon for Icon entry:" << iconEntry.value() << std::endl;
87                     return false;
88                 }
89
90                 return true;
91             }
92
93             bool deployCustomAppRunFile(const bf::path& customAppRunPath) const {
94                 // copy custom AppRun executable
95                 ldLog() << "Deploying custom AppRun:" << customAppRunPath << std::endl;
96
97                 const auto appRunPath = appDir.path() / "AppRun";
98
99                 if (!appDir.copyFile(customAppRunPath, appRunPath))
100                     return false;
101
102                 ldLog() << "Making AppRun file executable: " << appRunPath << std::endl;
103                 makeFileExecutable(appRunPath);
104
105                 return true;
106             }
107
108             bool deployStandardAppRunFromDesktopFile(const DesktopFile& desktopFile, const bf::path& customAppRunPath) const {
109                 // check if there is a custom AppRun already
110                 // in that case, skip deployment of symlink
111                 if (bf::exists(appDir.path() / "AppRun")) {
112                     ldLog() << LD_WARNING << "Existing AppRun detected, skipping deployment of symlink" << std::endl;
113                 } else {
114                     // look for suitable binary to create AppRun symlink
115                     DesktopFileEntry executableEntry;
116
117                     if (!desktopFile.getEntry("Desktop Entry", "Exec", executableEntry)) {
118                         ldLog() << LD_ERROR << "Exec entry missing in desktop file:" << desktopFile.path()
119                                 << std::endl;
120                         return false;
121                     }
122
123                     auto executableName = util::split(executableEntry.value())[0];
124
125                     const auto foundExecutablePaths = appDir.deployedExecutablePaths();
126
127                     if (foundExecutablePaths.empty()) {
128                         ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << executableName
129                                 << std::endl;
130                         return false;
131                     }
132
133                     bool deployedExecutable = false;
134
135                     for (const auto& executablePath : foundExecutablePaths) {
136                         ldLog() << LD_DEBUG << "Executable found:" << executablePath << std::endl;
137
138                         if (executablePath.filename() == executableName) {
139                             ldLog() << "Deploying AppRun symlink for executable in AppDir root:" << executablePath
140                                     << std::endl;
141
142                             if (!appDir.createRelativeSymlink(executablePath, appDir.path() / "AppRun")) {
143                                 ldLog() << LD_ERROR
144                                         << "Failed to create AppRun symlink for executable in AppDir root:"
145                                         << executablePath << std::endl;
146                                 return false;
147                             }
148
149                             deployedExecutable = true;
150                             break;
151                         }
152                     }
153
154                     if (!deployedExecutable) {
155                         ldLog() << LD_ERROR << "Could not deploy symlink for executable: could not find suitable executable for Exec entry:" << executableName << std::endl;
156                         return false;
157                     }
158                 }
159
160                 return true;
161             }
162
163             bool deployAppRunWrapperIfNecessary() const {
164                 const bf::path appRunPath(appDir.path() / "AppRun");
165                 const bf::path wrappedAppRunPath(appRunPath.string() + ".wrapped");
166
167                 const bf::path appRunHooksPath(appDir.path() / APPRUN_HOOKS_DIRNAME);
168
169                 // first, we check whether there's that special directory containing hooks
170                 if (!bf::is_directory(appRunHooksPath)) {
171                     ldLog() << LD_DEBUG << "Could not find apprun-hooks dir, no need to deploy the AppRun wrapper" << std::endl;
172                     return true;
173                 }
174
175                 // if there's no files in there we don't have to do anything
176                 bf::directory_iterator firstRegularFile = std::find_if(
177                     bf::directory_iterator(appRunHooksPath),
178                     bf::directory_iterator{},
179                     [](const bf::path& p) {
180                         return bf::is_regular_file(p);
181                     }
182                 );
183                 if (firstRegularFile == bf::directory_iterator{}) {
184                     ldLog() << LD_WARNING << "Found an empty apprun-hooks directory, assuming there is no need to deploy the AppRun wrapper" << std::endl;
185                     return true;
186                 }
187
188                 // any file within that directory is considered to be a script
189                 // we can't perform any validity checks, that would be way too much complexity and even tools which
190                 // claim they can, like e.g., shellcheck, aren't perfect, they only aid in avoiding bugs but cannot
191                 // prevent them completely
192
193                 // let's put together the wrapper script's contents
194                 std::ostringstream oss;
195
196                 oss << "#! /usr/bin/env bash" << std::endl
197                     << std::endl
198                     << "# autogenerated by makeappimage" << std::endl
199                     << std::endl
200                     << "# make sure errors in sourced scripts will cause this script to stop" << std::endl
201                     << "set -e" << std::endl
202                     << std::endl
203                     << "this_dir=\"$(readlink -f \"$(dirname \"$0\")\")\"" << std::endl
204                     << std::endl;
205
206                 std::for_each(bf::directory_iterator(appRunHooksPath), bf::directory_iterator{}, [&oss](const bf::path& p) {
207                     if (!bf::is_regular_file(p))
208                         return;
209
210                     oss << "source \"$this_dir\"/" << APPRUN_HOOKS_DIRNAME << "/" << p.filename() << std::endl;
211                 });
212
213                 oss << std::endl
214                     << "exec \"$this_dir\"/AppRun.wrapped \"$@\"" << std::endl;
215
216                 // first we need to make sure we're not running this more than once
217                 // this might cause more harm than good
218                 // we require the user to clean up the mess at first
219                 // FIXME: try to find a way how to rewrap AppRun on subsequent runs or, even better, become idempotent
220                 if (bf::exists(wrappedAppRunPath)) {
221                     ldLog() << LD_WARNING << "Already found wrapped AppRun, using existing file/symlink" << std::endl;
222                 } else {
223                     // backup original AppRun
224                     bf::rename(appRunPath, wrappedAppRunPath);
225                 }
226
227                 // in case the above check triggered a warning, it's possible that there is another AppRun in the AppDir
228                 // this one has to be cleaned up in that case
229                 if (bf::exists(appRunPath)) {
230                     ldLog() << LD_WARNING << "Found an AppRun file/symlink, possibly due to re-run of makeappimage, "
231                                             "overwriting" << std::endl;
232                     bf::remove(appRunPath);
233                 }
234
235
236                 // install new script
237                 std::ofstream ofs(appRunPath.string());
238                 ofs << oss.str();
239
240                 // make sure data is written to disk
241                 ofs.flush();
242                 ofs.close();
243
244                 // make new file executable
245                 makeFileExecutable(appRunPath);
246
247                 // we're done!
248                 return true;
249             }
250         };
251
252         AppDirRootSetup::AppDirRootSetup(const AppDir& appDir) : d(new Private(appDir)) {}
253
254         bool AppDirRootSetup::run(const DesktopFile& desktopFile, const bf::path& customAppRunPath) const {
255             // first step that is always required is to deploy the desktop file and the corresponding icon
256             if (!d->deployDesktopFileAndIcon(desktopFile)) {
257                 ldLog() << LD_DEBUG << "deployDesktopFileAndIcon returned false" << std::endl;
258                 return false;
259             }
260
261             // the algorithm depends on whether the user wishes to deploy their own AppRun file
262             // in case they do, the algorithm shall deploy that file
263             // otherwise, the standard algorithm shall be run which takes information from the desktop file to
264             // deploy a symlink pointing to the AppImage's main binary
265             // this allows power users to define their own AppImage initialization steps or run different binaries
266             // based on parameters etc.
267             if (!customAppRunPath.empty()) {
268                 if (!d->deployCustomAppRunFile(customAppRunPath)) {
269                     ldLog() << LD_DEBUG << "deployCustomAppRunFile returned false" << std::endl;
270                     return false;
271                 }
272             } else {
273                 if (!d->deployStandardAppRunFromDesktopFile(desktopFile, customAppRunPath)) {
274                     ldLog() << LD_DEBUG << "deployStandardAppRunFromDesktopFile returned false" << std::endl;
275                     return false;
276                 }
277             }
278             
279             // TODO: This code below can probably go, no plugin support anymore.
280             // plugins might need to run some initializing code to make certain features work
281             // these involve setting environment variables because libraries or frameworks don't support any other
282             // way of pointing them to resources inside the AppDir instead of looking into config files in locations
283             // inside the AppImage, etc.
284             // the makeappimage plugin specification states that if plugins put files into a specified directory in
285             // the AppImage, linuxdeploy will make sure they're run before running the regular AppRun
286             if (!d->deployAppRunWrapperIfNecessary()) {
287                 ldLog() << LD_DEBUG << "deployAppRunWrapperIfNecessary returned false" << std::endl;
288                 return false;
289             }
290
291             return true;
292         }
293     }
294 }
295
296
297