-
-
Notifications
You must be signed in to change notification settings - Fork 92
Expand file tree
/
Copy pathapprun3.py
More file actions
357 lines (273 loc) · 13.4 KB
/
apprun3.py
File metadata and controls
357 lines (273 loc) · 13.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
# Copyright 2022 Alexis Lopez Zubieta
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
import fnmatch
import logging
import os
import shlex
import shutil
import lief
from appimagebuilder.context import Context
from appimagebuilder.modules.setup import apprun_utils
from appimagebuilder.modules.setup.apprun_3.app_dir_info import AppDirFileInfo
from appimagebuilder.modules.setup.apprun_3.apprun3_context import AppRun3Context
from appimagebuilder.modules.setup.apprun_3.helpers.gdk_pixbuf import AppRun3GdkPixbuf
from appimagebuilder.modules.setup.apprun_3.helpers.glib import AppRun3GLib
from appimagebuilder.modules.setup.apprun_3.helpers.glibc_module import AppRun3GLibCSetupHelper
from appimagebuilder.modules.setup.apprun_3.helpers.glibstcpp_module import AppRun3GLibStdCppSetupHelper
from appimagebuilder.modules.setup.apprun_3.helpers.gstreamer import AppRun3GStreamer
from appimagebuilder.modules.setup.apprun_3.helpers.python import AppRun3Python
from appimagebuilder.modules.setup.apprun_3.helpers.mime import AppRun3MIME
from appimagebuilder.modules.setup.apprun_3.helpers.qt import AppRun3QtSetup
class AppRunV3Setup:
"""
AppRun v3 setup module
Configures an AppDir to use the AppRun v3 runtime format.
"""
def __init__(self, context: Context):
self.context = AppRun3Context(context)
def setup(self):
"""Configures the AppDir to use the AppRun v3 runtime format."""
# scan AppDir contents
self.context.app_dir.scan_files()
# resolve main architecture to know which AppRun binaries should be used later
self.context.main_arch = self._get_main_arch()
self._run_setup_helpers()
self.context.architectures.update(self.context.app_dir.architectures)
# patch scripts shebang to use embed interpreters
self._patch_scripts_shebang()
# deploy AppRun v3 runtime
self._deploy_librapprun_hooks_so()
self._deploy_apprun_bin()
self._deploy_apprun_config()
def _find_dirs_containing_libraries(self):
library_paths = set()
for file in self.context.app_dir.files.values():
# check if the binary is a library
if file.soname and not self._is_file_in_a_module(file):
# record the library dir path for later use in the apprun config generation
library_dir = file.path.parent
library_paths.add(library_dir)
return library_paths
def _is_file_in_a_module(self, file: AppDirFileInfo):
"""Checks if a file belongs to a module"""
path_str = file.path.__str__()
return path_str.startswith(self.context.modules_dir.__str__())
def _deploy_librapprun_hooks_so(self):
for arch in self.context.architectures:
self._deploy_libapprun_hooks_so(arch.name)
def _deploy_apprun_bin(self):
"""Deploys the AppRun binary for the main architecture"""
apprun_bin_path = self.context.binaries_resolver.resolve_executable(self.context.main_arch)
apprun_bin_target_path = self.context.app_dir.path / "AppRun"
shutil.copy(apprun_bin_path, apprun_bin_target_path)
# make binary executable
apprun_bin_target_path.chmod(0o755)
def _deploy_libapprun_hooks_so(self, arch):
"""Deploys the libapprun_hooks.so for a given architecture"""
libapprun_so_path = self.context.binaries_resolver.resolve_hooks_library(arch)
libapprun_so_target_path = self.get_hooks_library_target_path(arch)
shutil.copy(libapprun_so_path, libapprun_so_target_path)
def get_hooks_library_target_path(self, arch):
libapprun_so_target_dir = self._find_libapprun_hooks_so_target_dir(arch)
# provide a target dir if none was found
if not libapprun_so_target_dir:
libapprun_so_target_dir = self.context.app_dir.path / "lib" / arch
libapprun_so_target_dir.mkdir(parents=True, exist_ok=True)
# copy the libapprun_hooks.so to the target dir
libapprun_so_target_path = libapprun_so_target_dir / "libapprun_hooks.so"
return libapprun_so_target_path
def _find_libapprun_hooks_so_target_dir(self, arch):
"""Finds a suitable directory for the libapprun_hooks.so"""
base_dirs = [
self.context.app_dir.path / "lib",
self.context.app_dir.path / "lib64",
self.context.app_dir.path / "usr/lib",
self.context.app_dir.path / "usr/lib64",
]
# find dedicated folder for the architecture
for base_dir in base_dirs:
try:
for entry in base_dir.iterdir():
if entry.is_dir() and arch in entry.name:
return entry
except FileNotFoundError: # /usr/lib64 is often omitted
pass
return None
def _deploy_apprun_config(self):
"""Deploys the AppRun config file"""
exec_line = ["$APPDIR/" + self.context.app_info.exec]
if self.context.app_info.exec_args:
exec_line.extend(shlex.split(self.context.app_info.exec_args))
else:
exec_line.append("$@")
library_paths = self._find_dirs_containing_libraries()
library_paths = [self._replace_app_dir_in_path(path) for path in library_paths]
path_env = self._find_dirs_containing_executable_files()
self.context.runtime_env["PATH"] = ":".join(path_env) + ":$PATH"
self.context.runtime_env["LD_PRELOAD"] = str(self.get_hooks_library_target_path(self.context.main_arch))+":$LD_PRELOAD"
self._set_user_defined_env_vars()
self._replace_appdir_path_occurrences_in_env()
config = {
"version": "1.0",
"runtime": {
"exec": exec_line,
"library_paths": library_paths,
"path_mappings": self.context.path_mappings,
"environment": self.context.runtime_env,
},
}
if len(list(self.context.modules_dir.iterdir())) > 0:
config["runtime"]["modules_dir"] = (
"$APPDIR/"
+ self.context.modules_dir.relative_to(self.context.app_dir.path).__str__()
)
# write the config file
apprun_config_path = self.context.app_dir.path / "AppRun.config"
apprun_utils.write_config_file(config, apprun_config_path)
def _replace_appdir_path_occurrences_in_env(self):
app_dir_path_str = self.context.app_dir.path.__str__()
for k, v in self.context.runtime_env.items():
self.context.runtime_env[k] = v.replace(app_dir_path_str, "$APPDIR")
def _replace_appdir_path_by_environment_variable_in_paths(self, paths: [str]):
"""Replaces the appdir path by the $APPDIR environment variable in a list of paths"""
patched_paths = []
for path in paths:
appdir_path_str = self.context.app_dir.path.__str__()
if appdir_path_str in path:
new_path = path.replace(appdir_path_str, "$APPDIR")
patched_paths.append(new_path)
return sorted(patched_paths)
def _move_files_to_module_dir(self, files, target_module_dir):
"""Moves files to a module directory"""
new_file_paths = []
for entry in files:
relative_path = entry.relative_to(self.context.app_dir.path)
target_path = target_module_dir / relative_path
# ensure target dir exists
target_path.parent.mkdir(parents=True, exist_ok=True)
# move file to target dir
shutil.move(entry, target_path)
new_file_paths.append(target_path)
return new_file_paths
def _match_files_in_dir(self, patterns):
"""Matches files in a directory"""
matching_files = []
# iterate over all files in the app dir
search_queue = [self.context.app_dir.path]
while search_queue:
current_dir = search_queue.pop()
for entry in current_dir.iterdir():
if entry.is_dir():
search_queue.append(entry)
elif entry.is_file():
full_path = entry.__str__()
if any(fnmatch.fnmatch(full_path, pattern) for pattern in patterns):
matching_files.append(entry)
return matching_files
def _get_main_arch(self):
"""Resolves the main architecture"""
# check if there are user defined archictectures and use first one as main arch
if self.context.bundle_archs:
return next(iter(self.context.bundle_archs))
# get executable archictecture
executable_path = self.context.app_dir.path / self.context.app_info.exec
arch = self._get_executable_architecture(executable_path)
return arch
def _get_executable_architecture(self, executable_path):
"""Resolves the executable architecture"""
error_message_prefix = "Could not resolve executable architecture"
arch = None
iterations_count = 0
current_executable_path = executable_path
# follow interpreter links until we find a non-link, or we reach the max number of iterations
while iterations_count < 5 and not arch:
if not os.path.exists(current_executable_path):
raise Exception(
f"{error_message_prefix}, Could not find executable {current_executable_path} in AppDir"
)
binary = lief.parse(current_executable_path.__str__())
if binary:
arch = binary.header.machine_type.name
else:
# try read shebang
shebang = apprun_utils.read_shebang(current_executable_path)
if shebang:
rel_interpreter_path = shebang[0].lstrip("/")
current_executable_path = (
self.context.app_dir.path / rel_interpreter_path
)
else:
raise Exception(
f"{error_message_prefix}, not elf or script executable: {current_executable_path}"
)
if not arch:
raise Exception(
f"{error_message_prefix}, max iterations reached for: {executable_path}"
)
return arch
def _patch_scripts_shebang(self):
"""Patches the scripts shebang"""
# patch scripts shebang
for entry in self.context.app_dir.files.values():
if entry.shebang and not entry.path.is_symlink():
self._patch_script_shebang(entry)
def _patch_script_shebang(self, entry: AppDirFileInfo):
"""Patches a script shebang"""
if not entry.shebang:
return
rel_interpreter_path = entry.shebang[0].strip("/")
embed_interpreter_path = self.context.app_dir.path / rel_interpreter_path
if embed_interpreter_path.exists():
with open(entry.path.__str__(), "rb+") as f:
# assume that the shebang is not longer than 1024 bytes
chunk = f.read(1024)
if chunk[0:2] == b"#!":
chunk = apprun_utils.remove_left_slashes_on_shebang(chunk)
# write back the modified chunk
f.seek(0)
f.write(chunk)
logging.info("Patched script shebang: %s", entry.__str__())
else:
logging.warning("Script interpreter for %s not found in AppDir: %s", entry.path.__str__(), rel_interpreter_path)
def _find_dirs_containing_executable_files(self):
"""Finds the dirs containing executable files"""
executable_dirs = set()
for file in self.context.app_dir.files.values():
if file.is_executable and not self._is_file_in_a_module(file):
dir_path = file.path.parent.__str__()
executable_dirs.add(dir_path)
return executable_dirs
def _replace_app_dir_in_path(self, path):
"""Replaces the app dir in a path"""
path_str = str(path)
return path_str.replace(self.context.app_dir.path.__str__(), "$APPDIR")
def _run_setup_helpers(self):
"""Runs the setup helpers"""
helpers = [
AppRun3GLibCSetupHelper(self.context),
AppRun3GLibStdCppSetupHelper(self.context),
AppRun3QtSetup(self.context),
AppRun3GLib(self.context),
AppRun3GdkPixbuf(self.context),
AppRun3GStreamer(self.context),
AppRun3Python(self.context),
AppRun3MIME(self.context),
]
for helper in helpers:
helper.run()
def _set_user_defined_env_vars(self):
"""Sets the user defined environment variables"""
user_env = self.context.build_context.recipe.AppDir.runtime.env or {}
for k, v in user_env.items():
if k in self.context.runtime_env:
logging.warning("User defined environment variable overrides generated config: %s", k)
self.context.runtime_env[k] = v