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).
This commit is contained in:
Bruno Lopes 2026-01-13 19:45:33 -03:00
parent c71c160866
commit 0e2f0f6880
6 changed files with 132 additions and 20 deletions

View file

@ -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

View file

@ -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).

View file

@ -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'

View file

@ -39,5 +39,30 @@
<false/>
<key>NLSRequiresNativeExecution</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>xcf</string>
<string>XCF</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>image/x-xcf</string>
</array>
<key>CFBundleTypeIconFiles</key>
<array>
<string>fileicon-xcf.icns</string>
</array>
<key>CFBundleTypeName</key>
<string>eXperimental Computing Facility file</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
%FILE_TYPES%
</array>
</dict>
</plist>

View file

@ -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("<dict>\n")
if extensions:
outf.write(" <key>CFBundleTypeExtensions</key>\n")
outf.write(" <array>\n")
for ext in extensions:
outf.write(f" <string>{ext}</string>\n")
outf.write(f" <string>{ext.upper()}</string>\n")
outf.write(" </array>\n")
if mimes:
outf.write(" <key>CFBundleTypeMIMETypes</key>\n")
outf.write(" <array>\n")
for mime in mimes:
outf.write(f" <string>{mime}</string>\n")
outf.write(" </array>\n")
outf.write(" <key>CFBundleTypeIconFile</key>\n")
outf.write(" <string>fileicon</string>\n")
if "label" in values:
outf.write(" <key>CFBundleTypeName</key>\n")
outf.write(f" <string>{values['label']}</string>\n")
elif extensions:
outf.write(" <key>CFBundleTypeName</key>\n")
outf.write(f" <string>{extensions[0].upper()} file</string>\n")
outf.write(" <key>CFBundleTypeRole</key>\n")
outf.write(" <string>Viewer</string>\n")
outf.write(" <key>LSHandlerRank</key>\n")
outf.write(" <string>Default</string>\n")
outf.write("</dict>\n")
except Exception as e:
sys.stderr.write(f"(ERROR): When writing pseudo-UTI file {output_file}: {e}\n")
sys.exit(1)

View file

@ -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