From 0e2f0f6880edb6486bbf69b331fef02a14e4b266 Mon Sep 17 00:00:00 2001 From: Bruno Lopes Date: Tue, 13 Jan 2026 19:45:33 -0300 Subject: [PATCH] build/macos, plug-ins: Generate file associations for macOS automatically Following 2ce3c604 (for Windows) and d56676a2 (for Linux) To make this possible the generate_mime_ext.py internals were changed to construct a dictionary instead of a list like before, because macOS supports both extension and mimetype (and also UTI). --- .gitlab-ci.yml | 1 + build/macos/2_bundle-gimp-uni_base.py | 2 + build/macos/3_dist-gimp-apple.sh | 3 +- build/macos/Info.plist | 25 +++++++ plug-ins/generate_mime_ext.py | 103 +++++++++++++++++++++----- plug-ins/meson.build | 18 ++++- 6 files changed, 132 insertions(+), 20 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7dc51c8ed3..33d6232ab5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -706,6 +706,7 @@ gimp-macos-inhouse: - _build*/done-dylib.list # Needed by dist-mac-weekly - _build-*/config.h + - _build-*/plug-ins/file_associations_mac.list - _build-*/gimp-data/images/logo/gimp-dmg.png expire_in: 2 days diff --git a/build/macos/2_bundle-gimp-uni_base.py b/build/macos/2_bundle-gimp-uni_base.py index 634385c19f..0bd7ecd4cd 100644 --- a/build/macos/2_bundle-gimp-uni_base.py +++ b/build/macos/2_bundle-gimp-uni_base.py @@ -141,6 +141,8 @@ shutil.copy2(Path(f"{GIMP_SOURCE}/build/macos/Info.plist"), GIMP_DISTRIB) ### FIXME: Icon (generate Assets.car for Liquid Glass) (GIMP_DISTRIB / "Resources").mkdir(parents=True, exist_ok=True) shutil.copy2(Path(f"{os.getenv('MESON_BUILD_ROOT')}/gimp-data/images/logo/gimp.icns"), GIMP_DISTRIB / "Resources/AppIcon.icns") +shutil.copy2(Path(f"{os.getenv('MESON_BUILD_ROOT')}/build/macos/fileicon-xcf.icns"), GIMP_DISTRIB / "Resources/fileicon-xcf.icns") +shutil.copy2(Path(f"{os.getenv('MESON_BUILD_ROOT')}/build/macos/fileicon.icns"), GIMP_DISTRIB / "Resources/fileicon.icns") ## BUNDLE BASE (BARE MINIMUM TO RUN GTK APPS). diff --git a/build/macos/3_dist-gimp-apple.sh b/build/macos/3_dist-gimp-apple.sh index 8f08b71457..16d8cb1fd1 100644 --- a/build/macos/3_dist-gimp-apple.sh +++ b/build/macos/3_dist-gimp-apple.sh @@ -130,7 +130,8 @@ conf_plist "%BUNDLE_NAME%" "$BUNDLE_NAME" conf_plist "%GIMP_VERSION%" "$CUSTOM_GIMP_VERSION" #### Needed to differentiate on zsh etc conf_plist "%MUTEX_SUFFIX%" "$MUTEX_SUFFIX" -### FIXME: Configure associations +### List supported filetypes +sed -i '' "s|%FILE_TYPES%|$(tr -d '\n' < $BUILD_DIR/plug-ins/file_associations_mac.list)|g" "$DMG_MOUNT/$BUNDLE_NAME.app/Contents/Info.plist" ## 4.2 FIXME: Create .DS_Store to set .dmg background and icon layout printf '(INFO): generating .DS_Store\n' diff --git a/build/macos/Info.plist b/build/macos/Info.plist index be349640df..ff3234ebe1 100644 --- a/build/macos/Info.plist +++ b/build/macos/Info.plist @@ -39,5 +39,30 @@ NLSRequiresNativeExecution + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + xcf + XCF + + CFBundleTypeMIMETypes + + image/x-xcf + + CFBundleTypeIconFiles + + fileicon-xcf.icns + + CFBundleTypeName + eXperimental Computing Facility file + CFBundleTypeRole + Editor + LSHandlerRank + Default + + %FILE_TYPES% + diff --git a/plug-ins/generate_mime_ext.py b/plug-ins/generate_mime_ext.py index bd1325d9fb..6c497150c5 100644 --- a/plug-ins/generate_mime_ext.py +++ b/plug-ins/generate_mime_ext.py @@ -3,7 +3,7 @@ import sys import os import re -types_associations_list = set() +types_associations_dict = {} #Read file loading plug-ins sourcecode source_files = sys.argv[3:] @@ -16,39 +16,108 @@ for source_file in source_files: continue #Parse MIME types or extensions declared in the sourcecode - mode = sys.argv[1] - if mode == "--mime": - function_suffix = 'set_mime_types' - elif mode == "--association": - function_suffix = 'set_extensions' source_file_ext = os.path.splitext(source_file)[1].lower() if source_file_ext == '.c': if "LOAD_PROC" not in content and "load_procedure" not in content: continue - regex = (fr'gimp_file_procedure_{function_suffix}\s*' - fr'\(\s*GIMP_FILE_PROCEDURE\s*\(\s*procedure\s*\)\s*,\s*"([^"]+)"') + proc_regex = r'gimp_(load|vector_load)_procedure_new' + mime_regex = r'gimp_file_procedure_set_mime_types\s*\([^,]+,\s*"([^"]+)"' + ext_regex = r'gimp_file_procedure_set_extensions\s*\([^,]+,\s*"([^"]+)"' + label_regex = r'gimp_procedure_set_menu_label\s*\([^,]+,\s*_\("([^"]+)"\)\s*\)' elif source_file_ext == '.py': if "LoadProcedure" not in content: continue - regex = fr'procedure\.{function_suffix}\s*\(\s*"([^"]+)"\s*\)' + proc_regex = r'(LoadProcedure|VectorLoadProcedure)\.new' + mime_regex = r'procedure\.set_mime_types\s*\(\s*"([^"]+)"\s*\)' + ext_regex = r'procedure\.set_extensions\s*\(\s*"([^"]+)"\s*\)' + label_regex = r'procedure\.set_menu_label\s*\(\s*_\("([^"]+)"\)\s*\)' else: continue - for match in re.findall(regex, content, re.DOTALL): - #(Take care of extensions separated by commas) - for mime_extension in match.split(','): - trimmed = mime_extension.strip() - if trimmed: - types_associations_list.add(trimmed) + for proc_match in re.finditer(proc_regex, content): + #proc_name = proc_match.group(1).strip() + #key = (source_file, f"{proc_name}_{proc_match.start()}") + key = (source_file, proc_match.start()) + if key not in types_associations_dict: + types_associations_dict[key] = {"mime": set(), "extensions": set(), "label": set()} + mime_match = re.search(mime_regex, content[proc_match.end():]) + if mime_match: + #(Take care of mimes separated by commas) + for mime in mime_match.group(1).split(','): + trimmed_mime = mime.strip() + if trimmed_mime: + types_associations_dict[key]["mime"].add(trimmed_mime) + ext_match = re.search(ext_regex, content[proc_match.end():]) + if ext_match: + #(Take care of extensions separated by commas) + for ext in ext_match.group(1).split(','): + trimmed_ext = ext.strip() + if trimmed_ext: + types_associations_dict[key]["extensions"].add(trimmed_ext) + label_match = re.search(label_regex, content[proc_match.end():]) + if label_match: + types_associations_dict[key]["label"] = label_match.group(1).strip() +mode = sys.argv[1] if mode == "--mime": #Output string with the parsed MIME types - print(";".join(sorted(types_associations_list))) + all_mime = set() + for values in types_associations_dict.values(): + all_mime.update(values["mime"]) + print(";".join(sorted(all_mime))) elif mode == "--association": #Create list with the parsed extensions output_file = sys.argv[2] + all_ext = set() + for values in types_associations_dict.values(): + all_ext.update(values["extensions"]) try: with open(output_file, 'w', encoding='utf-8') as outf: - outf.writelines(f"{assoc}\n" for assoc in sorted(types_associations_list)) + outf.writelines(f"{assoc}\n" for assoc in sorted(all_ext)) except Exception as e: sys.stderr.write(f"(ERROR): When writing output file {output_file}: {e}\n") sys.exit(1) +elif mode == "--pseudo-uti": + output_file = sys.argv[2] + try: + with open(output_file, 'w', encoding='utf-8') as outf: + for (source_file, proc_key), values in types_associations_dict.items(): + extensions = sorted(values["extensions"]) + mimes = sorted(values["mime"]) + if not extensions and not mimes: + continue + + outf.write("\n") + if extensions: + outf.write(" CFBundleTypeExtensions\n") + outf.write(" \n") + for ext in extensions: + outf.write(f" {ext}\n") + outf.write(f" {ext.upper()}\n") + outf.write(" \n") + + if mimes: + outf.write(" CFBundleTypeMIMETypes\n") + outf.write(" \n") + for mime in mimes: + outf.write(f" {mime}\n") + outf.write(" \n") + + outf.write(" CFBundleTypeIconFile\n") + outf.write(" fileicon\n") + + if "label" in values: + outf.write(" CFBundleTypeName\n") + outf.write(f" {values['label']}\n") + elif extensions: + outf.write(" CFBundleTypeName\n") + outf.write(f" {extensions[0].upper()} file\n") + + outf.write(" CFBundleTypeRole\n") + outf.write(" Viewer\n") + + outf.write(" LSHandlerRank\n") + outf.write(" Default\n") + outf.write("\n") + except Exception as e: + sys.stderr.write(f"(ERROR): When writing pseudo-UTI file {output_file}: {e}\n") + sys.exit(1) diff --git a/plug-ins/meson.build b/plug-ins/meson.build index 19a2b9355a..9c2595e12b 100644 --- a/plug-ins/meson.build +++ b/plug-ins/meson.build @@ -62,7 +62,7 @@ if get_option('webkit-unmaintained') 'name': 'help-browser', } endif - + if platform_windows and host_cpu_family == 'x86' complex_plugins_list += { 'name': 'twain', @@ -87,7 +87,7 @@ endforeach subdir('python') foreach plugin : python_plugins - if plugin.get('name').startswith('file-') + if plugin.get('name').startswith('file-') all_plugins_sources += [ meson.current_source_dir() / 'python' / plugin.get('name') + '.py' ] endif endforeach @@ -116,3 +116,17 @@ if get_option('windows-installer') or get_option('ms-store') build_by_default: true ) endif + +if get_option('dmg') + custom_target('file_associations_mac', + input : all_plugins_sources, + output : 'file_associations_mac.list', + command : [ + python, + meson.current_source_dir() / 'generate_mime_ext.py', + '--pseudo-uti', + '@OUTPUT@', + ] + all_plugins_sources, + build_by_default: true + ) +endif