Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package gearlever for openSUSE:Factory checked in at 2025-11-09 21:10:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/gearlever (Old) and /work/SRC/openSUSE:Factory/.gearlever.new.1980 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "gearlever" Sun Nov 9 21:10:10 2025 rev:16 rq:1316678 version:3.4.7 Changes: -------- --- /work/SRC/openSUSE:Factory/gearlever/gearlever.changes 2025-10-14 18:10:08.059401732 +0200 +++ /work/SRC/openSUSE:Factory/.gearlever.new.1980/gearlever.changes 2025-11-09 21:12:38.695047040 +0100 @@ -1,0 +2,9 @@ +Sun Nov 9 13:54:04 UTC 2025 - Jaime Marquínez Ferrándiz <[email protected]> + +- Update to 3.4.7: + * Fix updating from Codeberg and GitLab + * Fixed unescaped \ char +- Update to 3.4.6: + * Improved desktop file support: added support for desktop actions + +------------------------------------------------------------------- Old: ---- gearlever-3.4.5.tar.gz New: ---- gearlever-3.4.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ gearlever.spec ++++++ --- /var/tmp/diff_new_pack.2zEqwp/_old 2025-11-09 21:12:39.287071800 +0100 +++ /var/tmp/diff_new_pack.2zEqwp/_new 2025-11-09 21:12:39.287071800 +0100 @@ -18,7 +18,7 @@ %define appid it.mijorus.gearlever Name: gearlever -Version: 3.4.5 +Version: 3.4.7 Release: 0 Summary: Manage AppImages License: GPL-3.0-or-later ++++++ gearlever-3.4.5.tar.gz -> gearlever-3.4.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/data/it.mijorus.gearlever.appdata.xml.in new/gearlever-3.4.7/data/it.mijorus.gearlever.appdata.xml.in --- old/gearlever-3.4.5/data/it.mijorus.gearlever.appdata.xml.in 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/data/it.mijorus.gearlever.appdata.xml.in 2025-10-30 12:31:49.000000000 +0100 @@ -20,6 +20,16 @@ <p>An utility to manage AppImages with ease! Gear lever will organize and manage AppImage files for you, generate desktop entries and app metadata, update apps in-place or keep multiple versions side-by-side.</p> </description> <releases> + <release type="stable" version="3.4.7" date="2025-11-30:00:00Z"> + <description> + <p>- Bug fix</p> + </description> + </release> + <release type="stable" version="3.4.6" date="2025-11-29:00:00Z"> + <description> + <p>- Improved desktop file support: added support for desktop actions</p> + </description> + </release> <release type="stable" version="3.4.5" date="2025-10-09:00:00Z"> <description> <p>- Fixed an issue happening on systems with more than one user</p> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/meson.build new/gearlever-3.4.7/meson.build --- old/gearlever-3.4.5/meson.build 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/meson.build 2025-10-30 12:31:49.000000000 +0100 @@ -1,5 +1,5 @@ project('gearlever', - version: '3.4.5', + version: '3.4.7', meson_version: '>= 0.59.0', default_options: [ 'warning_level=2', ], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/src/AppDetails.py new/gearlever-3.4.7/src/AppDetails.py --- old/gearlever-3.4.5/src/AppDetails.py 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/src/AppDetails.py 2025-10-30 12:31:49.000000000 +0100 @@ -123,6 +123,7 @@ self.env_variables_widgets = [] self.env_variables_group_container = None self.save_vars_btn: Optional[Gtk.Button] = None + self.command_line_arguments_row = None # Update url entry self.update_url_group: Optional[Adw.PreferencesGroup] = None @@ -195,7 +196,8 @@ if self.app_list_element.installed_status is InstalledStatus.INSTALLED: # Exec arguments - gtk_list.append(self.create_show_exec_args_row()) + self.command_line_arguments_row = self.create_show_exec_args_row() + gtk_list.append(self.command_line_arguments_row) # A custom link to a website gtk_list.append(self.create_edit_custom_website_row()) @@ -251,6 +253,10 @@ @_async def load(self, load_completed_callback: Optional[Callable] = None): self.show_row_spinner(True) + + if self.command_line_arguments_row: + self.command_line_arguments_row.remove_css_class('error') + icon = Gtk.Image(icon_name='application-x-executable-symbolic') generation = self.provider.get_appimage_type(self.app_list_element) @@ -269,10 +275,10 @@ @_async def install_file(self, el: AppImageListElement): - try: - self.provider.install_file(el) - except Exception as e: - logging.error(str(e)) + self.provider.install_file(el) + # try: + # except Exception as e: + # logging.error(str(e)) self.update_installation_status() @@ -691,9 +697,14 @@ def on_cmd_arguments_changed(self, widget): text = widget.get_text().strip() text = text.replace('\n', '') + widget.remove_css_class('error') - self.app_list_element.exec_arguments = shlex.split(text) - self.provider.update_desktop_file(self.app_list_element) + self.app_list_element.exec_arguments = text + + try: + self.provider.update_desktop_file(self.app_list_element) + except: + widget.add_css_class('error') # Returns the configuration from the json for this specific app def get_config_for_app(self) -> dict: @@ -865,7 +876,7 @@ row = Adw.EntryRow( title=(_('Command line arguments')), selectable=False, - text=' '.join(self.app_list_element.exec_arguments) + text=self.app_list_element.exec_arguments ) row_img = Gtk.Image(icon_name='gearlever-cmd-args', pixel_size=self.ACTION_ROW_ICON_SIZE) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/src/main.py new/gearlever-3.4.7/src/main.py --- old/gearlever-3.4.5/src/main.py 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/src/main.py 2025-10-30 12:31:49.000000000 +0100 @@ -160,6 +160,9 @@ if not os.path.exists(LOG_FOLDER): os.makedirs(LOG_FOLDER) + if not os.path.exists(TMP_DIR): + os.makedirs(TMP_DIR) + # Clear log file if it's too big log_file_size = 0 if os.path.exists(LOG_FILE): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/src/models/UpdateManager.py new/gearlever-3.4.7/src/models/UpdateManager.py --- old/gearlever-3.4.5/src/models/UpdateManager.py 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/src/models/UpdateManager.py 2025-10-30 12:31:49.000000000 +0100 @@ -489,10 +489,12 @@ if paths[1] != 'api' or paths[2] != 'v4' or paths[5] != 'packages': return False - return { - 'username': paths[4], - 'filename': paths[9], - } + return { + 'username': paths[4], + 'filename': paths[9], + } + + return False def can_handle_link(url: str): return GitlabUpdater.get_url_data(url) != False @@ -638,11 +640,13 @@ if len(paths) != 7: return False - return { - 'username': paths[1], - 'repo': paths[2], - 'filename': paths[6], - } + return { + 'username': paths[1], + 'repo': paths[2], + 'filename': paths[6], + } + + return False def can_handle_link(url: str): return CodebergUpdater.get_url_data(url) != False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gearlever-3.4.5/src/providers/AppImageProvider.py new/gearlever-3.4.7/src/providers/AppImageProvider.py --- old/gearlever-3.4.5/src/providers/AppImageProvider.py 2025-10-09 10:35:37.000000000 +0200 +++ new/gearlever-3.4.7/src/providers/AppImageProvider.py 2025-10-30 12:31:49.000000000 +0100 @@ -4,7 +4,7 @@ import shutil import filecmp import shlex -from xdg import DesktopEntry +from xdg.DesktopEntry import DesktopEntry import dataclasses from ..lib.constants import APP_ID, TMP_DIR @@ -22,7 +22,7 @@ class ExtractedAppImage(): extraction_folder: str - desktop_entry: Optional[DesktopEntry.DesktopEntry] + desktop_entry: Optional[DesktopEntry] appimage_file: Gio.File desktop_file: Optional[Gio.File] icon_file: Optional[Gio.File] @@ -47,8 +47,8 @@ trusted: bool = False is_updatable_from_url = False env_variables: List[str] = dataclasses.field(default_factory=lambda: []) - exec_arguments: List[str] = dataclasses.field(default_factory=lambda: []) - desktop_entry: Optional[DesktopEntry.DesktopEntry] = None + exec_arguments: str = '' + desktop_entry: Optional[DesktopEntry] = None update_logic: Optional[AppImageUpdateLogic] = None architecture: Optional[AppImageArchitecture] = None updating_from: Optional[any] = None # AppImageListElement @@ -99,7 +99,7 @@ try: if os.path.isfile(gfile.get_path()) and get_giofile_content_type(gfile) == 'application/x-desktop': - entry = DesktopEntry.DesktopEntry(filename=gfile.get_path()) + entry = DesktopEntry(filename=gfile.get_path()) exec_location = entry.getTryExec() exec_command_data = extract_terminal_arguments(entry.getExec()) @@ -121,7 +121,7 @@ desktop_entry=entry, trusted=True, external_folder=(not exec_in_defalut_folder), - exec_arguments=exec_command_data['arguments'], + exec_arguments=shlex.join(exec_command_data['arguments']), env_variables=exec_command_data['env_vars'], ) @@ -346,69 +346,82 @@ if not os.path.exists(self.user_desktop_files_path): os.makedirs(self.user_desktop_files_path) - dest_desktop_file_path = f'{os.path.join(self.user_desktop_files_path, prefixed_filename)}.desktop' + dest_desktop_file_path = os.path.join(self.user_desktop_files_path, prefixed_filename) + '.desktop' dest_desktop_file_path = dest_desktop_file_path.replace(' ', '_') # Get default exec arguments exec_arguments = shlex.split(extracted_appimage.desktop_entry.getExec())[1:] - el.exec_arguments = exec_arguments + el.exec_arguments = shlex.join(exec_arguments) + desktop_file_content = '' with open(extracted_appimage.desktop_file.get_path(), 'r') as dskt_file: desktop_file_content = dskt_file.read() - desktop_file_content = re.sub(r'^TryExec=.*$', "", desktop_file_content, flags=re.MULTILINE) - desktop_file_content = re.sub(r'^Icon=.*$', "", desktop_file_content, flags=re.MULTILINE) - desktop_file_content = re.sub(r'^X-AppImage-Version=.*$', "", desktop_file_content, flags=re.MULTILINE) - - # replace executable path - exec_command = ['Exec=' + shlex.join([dest_appimage_file.get_path(), *exec_arguments])] - # replace try exec executable path - exec_command.append(f'TryExec=' + dest_appimage_file.get_path()) - if dest_appimage_icon_file: - exec_command.append(f"Icon={dest_appimage_icon_file.get_path()}") + escaped_exec_filepath = self._escape_exec_argument(dest_appimage_file.get_path()) + icon_key = 'Icon=applications-other' + if dest_appimage_icon_file: + icon_key = dest_appimage_icon_file.get_path() + + for g in extracted_appimage.desktop_entry.groups(): + if g == DesktopEntry.defaultGroup: + exec_key = extracted_appimage.desktop_entry.get('Exec', group=g) + exec_kg_arguments = shlex.split(exec_key)[1:] + + exec_line = ' '.join([ + escaped_exec_filepath, + shlex.join(exec_kg_arguments) + ]) + + desktop_file_content = self._desktop_file_replace_key(desktop_file_content, g, 'Exec', exec_line) + desktop_file_content = self._desktop_file_replace_key(desktop_file_content, g, 'TryExec', dest_appimage_file.get_path()) + desktop_file_content = self._desktop_file_replace_key(desktop_file_content, g, 'Icon', icon_key) else: - exec_command.append(f'Icon=applications-other') + if extracted_appimage.desktop_entry.hasKey('Icon', group=g): + desktop_file_content = self._desktop_file_replace_key(desktop_file_content, g, 'Icon', icon_key) - desktop_file_content = re.sub( - r'^Exec=.*$', - '\n'.join(exec_command), - desktop_file_content, - flags=re.MULTILINE - ) + if extracted_appimage.desktop_entry.hasKey('Exec', group=g): + exec_key = extracted_appimage.desktop_entry.get('Exec', group=g) + exec_kg_arguments = shlex.split(exec_key)[1:] + exec_line = ' '.join([ + escaped_exec_filepath, + shlex.join(exec_kg_arguments) + ]) - # generate a new app name - final_app_name = extracted_appimage.appimage_file.get_basename() - if extracted_appimage.desktop_entry: - final_app_name = extracted_appimage.desktop_entry.getName() - desktop_file_content += f'\nX-AppImage-Version={version}' + desktop_file_content = self._desktop_file_replace_key( + desktop_file_content, g, 'Exec', exec_line) - if el.update_logic is AppImageUpdateLogic.KEEP: - final_app_name += f' ({version})' + # generate a new app name + final_app_name = extracted_appimage.appimage_file.get_basename() + if extracted_appimage.desktop_entry: + final_app_name = extracted_appimage.desktop_entry.get('Name', group=DesktopEntry.defaultGroup) - desktop_file_content = re.sub( - r'^Name\[(.*?)\]=.*$', - '', - desktop_file_content, - flags=re.MULTILINE - ) - - final_app_name = final_app_name.strip() - desktop_file_content = re.sub( - r'^Name=.*$', - f"Name={final_app_name}", - desktop_file_content, - flags=re.MULTILINE - ) + if el.update_logic is AppImageUpdateLogic.KEEP: + final_app_name += f' ({version})' + + desktop_file_content = self._desktop_file_replace_key( + desktop_file_content, + group_name=DesktopEntry.defaultGroup, + key='X-AppImage-Version', + replacement=version + ) + + final_app_name = final_app_name.strip() + desktop_file_content = self._desktop_file_replace_key( + desktop_file_content, + group_name=DesktopEntry.defaultGroup, + key='Name', + replacement=final_app_name + ) - # finally, write the new .desktop file - if (not os.path.exists(self.user_desktop_files_path)) and os.path.exists(self.user_local_share_path): - os.mkdir(self.user_desktop_files_path) + # finally, write the new .desktop file + if (not os.path.exists(self.user_desktop_files_path)) and os.path.exists(self.user_local_share_path): + os.mkdir(self.user_desktop_files_path) - with open(dest_desktop_file_path, 'w+') as desktop_file_python_dest: - desktop_file_python_dest.write(desktop_file_content) + with open(dest_desktop_file_path, 'w+') as desktop_file_python_dest: + desktop_file_python_dest.write(desktop_file_content) if os.path.exists(dest_desktop_file_path): - el.desktop_entry = DesktopEntry.DesktopEntry(filename=dest_desktop_file_path) + el.desktop_entry = DesktopEntry(filename=dest_desktop_file_path) el.desktop_file_path = dest_desktop_file_path el.installed_status = InstalledStatus.INSTALLED @@ -513,67 +526,47 @@ shutil.rmtree(self.extraction_folder) os.makedirs(self.extraction_folder) - def update_exec_arguments(self, el:AppImageListElement, arg_string: str): - arg_string = arg_string.replace("\n", "") - + def update_desktop_file(self, el: AppImageListElement): if not el.desktop_file_path: raise Exception('desktop_file_path not specified') - - desktop_file_content = '' - entry = DesktopEntry.DesktopEntry(filename=el.desktop_file_path) - with open(el.desktop_file_path, 'r') as desktop_file: - desktop_file_content = desktop_file.read() - exec_command = shlex.split(entry.getExec())[0] - exec_command += f' {arg_string}' - # replace executable path - desktop_file_content = re.sub( - r'^Exec=.*$', - f"Exec={exec_command}", - desktop_file_content, - flags=re.MULTILINE - ) + entry = DesktopEntry(filename=el.desktop_file_path) + tryexec_command = entry.getTryExec() + exec_arguments = el.exec_arguments + env_vars = ' '.join(el.env_variables) + + if exec_arguments: + exec_arguments = f' {exec_arguments}' + + if env_vars: + env_vars = f'env {env_vars} ' + + exec_command = ''.join([ + env_vars, + self._escape_exec_argument(tryexec_command), + exec_arguments + ]) - with open(el.desktop_file_path, 'w') as desktop_file: - desktop_file.write(desktop_file_content) - - def update_desktop_file(self, el: AppImageListElement): - if not el.desktop_file_path: - raise Exception('desktop_file_path not specified') - desktop_file_content = '' - entry = DesktopEntry.DesktopEntry(filename=el.desktop_file_path) with open(el.desktop_file_path, 'r') as desktop_file: desktop_file_content = desktop_file.read() - tryexec_command = entry.getTryExec() - exec_arguments = ' '.join(el.exec_arguments) - env_vars = ' '.join(el.env_variables) - - if exec_arguments: - exec_arguments = f' {exec_arguments}' - - if env_vars: - env_vars = f'env {env_vars} ' - - exec_command = ''.join([ - env_vars, - shlex.quote(tryexec_command), - exec_arguments - ]) - - # replace executable path - desktop_file_content = re.sub( - r'^Exec=.*$', - f"Exec={exec_command}", - desktop_file_content, - flags=re.MULTILINE - ) + # replace executable path + desktop_file_content = self._desktop_file_replace_key( + desktop_file_content, + group_name=DesktopEntry.defaultGroup, + key='Exec', + replacement=exec_command + ) - with open(el.desktop_file_path, 'w') as desktop_file: + tmp_desktop_file = os.path.join(TMP_DIR, get_random_string() + '.desktop') + with open(tmp_desktop_file, 'w+') as desktop_file: desktop_file.write(desktop_file_content) - el.desktop_entry = DesktopEntry.DesktopEntry(filename=el.desktop_file_path) + terminal.host_sh(['desktop-file-validate', '--no-hints', '--no-warn-deprecated', tmp_desktop_file]) + + shutil.move(tmp_desktop_file, el.desktop_file_path) + el.desktop_entry = DesktopEntry(filename=el.desktop_file_path) def update_from_url(self, manager, el: AppImageListElement, status_cb: callable) -> AppImageListElement: try: @@ -600,6 +593,45 @@ return list_element # Private methods + def _escape_exec_argument(self, arg: str) -> str: + """ + Escape an input string according to the Exec key quoting rules + from the freedesktop.org Desktop Entry Specification. + + Escapes: + - Double quote (") + - Backtick (`) + - Dollar sign ($) + - Backslash (\\) + + Returns the escaped string, enclosed in double quotes if needed. + """ + reserved_chars = set(' \t\n"\'\\><~|&;$*?#()`') + + def escape_inner(s: str) -> str: + s = s.replace("\\", "\\\\") # Escape backslash first + s = s.replace('"', r'\"') + s = s.replace('`', r'\`') + s = s.replace('$', r'\$') + return s + + if any(ch in reserved_chars for ch in arg): + return f'"{escape_inner(arg)}"' + else: + return arg + + def _desktop_file_replace_key(self, content: str, group_name: str, key: str, replacement: str): + pattern = rf'(\[{group_name}\][\s\S]*?)^{key}=.*$' + + if re.search(pattern, content, flags=re.MULTILINE): + replacement = rf'\1{key}={replacement}' + else: + pattern = rf'\[{group_name}\].*$' + replacement = f'[{group_name}]\n{key}={replacement}' + + + return re.sub(pattern, replacement, content, flags=re.MULTILINE, count=1) + def _run_filepath(self, el: AppImageListElement): is_nixos = re.search(r"^NAME=NixOS$", get_osinfo(), re.MULTILINE) != None @@ -708,7 +740,6 @@ appimage_offset = terminal.sandbox_sh(['get_appimage_offset', file.get_path()]) try: - terminal.sandbox_sh(['unsquashfs', '-o', appimage_offset, '-l', file.get_path()]) terminal.sandbox_sh(['unsquashfs', '-o', appimage_offset, '-d', squashfs_root_folder, file.get_path()]) except Exception as e: logging.error('Extraction with unsquashfs failed') @@ -732,7 +763,7 @@ icon_file: Optional[Gio.File] = None desktop_file: Optional[Gio.File] = None - desktop_entry: Optional[DesktopEntry.DesktopEntry] = None + desktop_entry: Optional[DesktopEntry] = None # hash file md5_hash = get_file_hash(file) @@ -763,7 +794,7 @@ desktop_entry_icon = None if desktop_file: - desktop_entry = DesktopEntry.DesktopEntry(desktop_file.get_path()) + desktop_entry = DesktopEntry(desktop_file.get_path()) desktop_entry_icon = desktop_entry.getIcon() desktop_entry_icon = re.sub(r"\.(png|svg)$", '', desktop_entry_icon)
