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