🧋 add array support for doante link metadata.

With this change the `Donate` metadata field can also be an array of
links, not just one single link. e.g.:

```
Donate:
  - "https://f-droid.org/donate"
  - "https://donate.example.com/fdroid"
```

For backwards compatibility the old solo string format is also
supported. e.g.:

```
Donate: "https://f-droid.org/donate"
```
This commit is contained in:
Michael Pöhn committed 2026-08-27 13:54:58 +00:00
1 parent ab268be1c8
commit d7f52c4580
12 files changed
+59 -26

No files matched your search

+5
View File
@@ -61,6 +61,7 @@ metadata_v0:
GIT_DEPTH: 1000
RELEASE_COMMIT_ID: d172d991803b27ad5213d11b5a737e5cb9e5fc3a # after switching to `git rev-parse`
script:
- apt update -yyq && apt install -yyq yq
- git fetch https://gitlab.com/fdroid/fdroidserver.git $RELEASE_COMMIT_ID
- cd tests
- export GITCOMMIT=$(git rev-parse HEAD)
@@ -82,6 +83,10 @@ metadata_v0:
metadata/dump_*/*.yaml
# remove leading zeros (because we can't deal with that reliably)
- 'sed -i ''s/: 0*\([1-9][0-9]*\)/: \1/g'' metadata/dump_*/*.yaml'
# if .Donate is an array set .Donate to the first element in the array otherwise leave it unchanged
# (running this in batches significantly speeds the gitlab.ci run)
# TODO: remove this when tests need to migrate
- find metadata/dump_*/ -name '*.yaml' -print0 | xargs -0 -P 4 -n 50 -- yq -y --in-place '.Donate |= if type == "array" then .[0] else . end'
- diff -uw metadata/dump_*
.apt-template: &apt-template
+12 -2
View File
@@ -607,7 +607,7 @@ def package_metadata(app, repodir):
del meta["license"]
if app["Donate"]:
meta["donate"] = [app["Donate"]]
meta["donate"] = app["Donate"]
# TODO handle different resolutions
if app.get("icon"):
@@ -991,6 +991,9 @@ def make_v1(apps, packages, repodir, repodict, requestsdict, signer_fingerprints
v = str(v)
elif k == 'CurrentVersion': # TODO make SuggestedVersionName the canonical name
k = 'suggestedVersionName'
elif k == "Donate":
k = "donate"
v = _first(v)
else:
k = k[:1].lower() + k[1:]
d[k] = v
@@ -1327,7 +1330,7 @@ def make_v0(apps, apks, repodir, repodict, requestsdict, signer_fingerprints):
addElementNonEmpty('changelog', app.Changelog, doc, apel)
addElementNonEmpty('author', app.AuthorName, doc, apel)
addElementNonEmpty('email', app.AuthorEmail, doc, apel)
addElementNonEmpty('donate', app.Donate, doc, apel)
addElementNonEmpty('donate', _first(app.Donate), doc, apel)
addElementNonEmpty('bitcoin', app.Bitcoin, doc, apel)
addElementNonEmpty('litecoin', app.Litecoin, doc, apel)
addElementNonEmpty('openCollective', app.OpenCollective, doc, apel)
@@ -2116,3 +2119,10 @@ def make_altstore(apps, apks, config, repodir, pretty=False):
with open(Path(repodir) / 'altstore-index.json', "w", encoding="utf-8") as f:
json.dump(idx, f, indent=indent)
def _first(items, default_value=None):
try:
return items[0]
except (IndexError, TypeError):
return default_value
+6
View File
@@ -1782,6 +1782,12 @@ def check_regexes(app):
for line in v.splitlines():
if m.match(line):
yield "%s at line '%s': %s" % (f, line, r)
elif t == metadata.TYPE_LIST:
for item in v or []:
if item is None:
continue
if m.match(item):
yield "%s '%s': %s" % (f, item, r)
else:
if v is None:
continue
+7 -1
View File
@@ -126,7 +126,7 @@ class App(dict):
self.IssueTracker = ''
self.Translation = ''
self.Changelog = ''
self.Donate = None
self.Donate = []
self.Liberapay = None
self.OpenCollective = None
self.Bitcoin = None
@@ -188,6 +188,7 @@ fieldtypes = {
'MaintainerNotes': TYPE_MULTILINE,
'Categories': TYPE_LIST,
'AntiFeatures': TYPE_STRINGMAP,
'Donate': TYPE_LIST,
'RequiresRoot': TYPE_BOOL,
'AllowedAPKSigningKeys': TYPE_LIST,
'Builds': TYPE_BUILD,
@@ -1269,6 +1270,11 @@ def _app_to_yaml(app):
v = _format_stringmap(app['id'], field, value)
if v:
cm[field] = v
elif field == 'Donate':
if len(value) == 1:
cm[field] = value[0]
else:
cm[field] = value
elif field == 'AllowedAPKSigningKeys':
value = [str(i).lower() for i in value]
if len(value) == 1:
+9 -9
View File
@@ -1052,44 +1052,44 @@ def insert_funding_yml_donation_links(apps):
if k == 'custom':
s = sanitize_funding_yml_entry(v)
if s:
app['Donate'] = s
app['Donate'] = [s]
break
elif k in ('community_bridge', 'lfx_crowdfunding'):
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = (
app['Donate'] = [
f'https://crowdfunding.lfx.linuxfoundation.org/projects/{s}'
)
]
break
elif k == 'github':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://github.com/sponsors/' + s
app['Donate'] = ['https://github.com/sponsors/' + s]
break
elif k == 'issuehunt':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://issuehunt.io/r/' + s
app['Donate'] = ['https://issuehunt.io/r/' + s]
break
elif k == 'ko_fi':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://ko-fi.com/' + s
app['Donate'] = ['https://ko-fi.com/' + s]
break
elif k == 'patreon':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://patreon.com/' + s
app['Donate'] = ['https://patreon.com/' + s]
break
elif k == 'buy_me_a_coffee':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://www.buymeacoffee.com/' + s
app['Donate'] = ['https://www.buymeacoffee.com/' + s]
break
elif k == 'polar':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://polar.sh/' + s
app['Donate'] = ['https://polar.sh/' + s]
break
@@ -346,7 +346,7 @@ Description: 'To configure, go to "Settings => Accounts & Sync => Add Account".
Appbrain SDK was removed before building.'
Disabled: null
Donate: null
Donate: []
IssueTracker: https://github.com/loadrunner/Facebook-Contact-Sync/issues
Liberapay: null
License: GPL-3.0-only
+1 -1
View File
@@ -169,7 +169,7 @@ CurrentVersion: '1.5'
CurrentVersionCode: 6
Description: Activates silent mode during calendar events.
Disabled: null
Donate: null
Donate: []
IssueTracker: https://github.com/miguelvps/PoliteDroid/issues
Liberapay: null
License: GPL-3.0-only
+2 -1
View File
@@ -1114,7 +1114,8 @@ Description: 'An ad blocker that uses the hosts file. The hosts file
read-only.'
Disabled: null
Donate: http://sufficientlysecure.org/index.php/adaway
Donate:
- http://sufficientlysecure.org/index.php/adaway
IssueTracker: https://github.com/dschuermann/ad-away/issues
Liberapay: null
License: GPL-3.0-only
@@ -384,7 +384,7 @@ Description: 'SMSSecure is an SMS/MMS application that allows you to protect you
* Open Source. SMSSecure is Free and Open Source, enabling anyone to verify its
security by auditing the code.'
Disabled: null
Donate: null
Donate: []
IssueTracker: https://github.com/SMSSecure/SMSSecure/issues
Liberapay: null
License: GPL-3.0-only
+2 -1
View File
@@ -2605,7 +2605,8 @@ Description: 'Video and audio player that supports a wide range of formats,
'
Disabled: null
Donate: http://www.videolan.org/contribute.html#money
Donate:
- http://www.videolan.org/contribute.html#money
IssueTracker: http://www.videolan.org/support/index.html#bugs
Liberapay: null
License: GPL-3.0-only
+2 -2
View File
@@ -179,13 +179,13 @@ class LintTest(SetUpTearDownMixin, unittest.TestCase):
def test_check_regexes_donate(self):
app = fdroidserver.metadata.App()
app.Donate = 'https://example.com/donate/'
app.Donate = ['https://example.com/donate/']
for warn in fdroidserver.lint.check_regexes(app):
self.fail()
def test_check_regexes_donate_both(self):
app = fdroidserver.metadata.App()
app.Donate = 'http://bit.ly/givememoney/'
app.Donate = ['http://bit.ly/givememoney/']
warns = list(fdroidserver.lint.check_regexes(app))
for warn in warns:
self.assertIn('bit.ly', warn)
+11 -7
View File
@@ -1615,7 +1615,7 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
'CurrentVersion': '',
'CurrentVersionCode': None,
'Disabled': '',
'Donate': '',
'Donate': None,
'IssueTracker': '',
'License': '',
'Litecoin': '',
@@ -1665,7 +1665,7 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertIsNotNone(app.get(field), field)
self.assertEqual('LINK1', app.get('Donate'))
self.assertEqual(['LINK1'], app.get('Donate'))
self.assertEqual('USERNAME', app.get('Liberapay'))
self.assertEqual('USERNAME', app.get('OpenCollective'))
@@ -1686,8 +1686,9 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
apps = {app.id: app}
os.mkdir(os.path.join('build', app.id))
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertIsNone(app.get(field))
self.assertEqual(app.get("Donate"), [])
self.assertIsNone(app.get("Liberapay"))
self.assertIsNone(app.get("OpenCollective"))
content = textwrap.dedent(
"""
@@ -1715,8 +1716,10 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
elif 'open_collective' in data:
self.assertEqual(data['open_collective'], app.get('OpenCollective'))
else:
# 'Donate' metadata
for v in data.values():
self.assertEqual(app.get('Donate', '').split('/')[-1], v)
for donate_option in app.get('Donate', []):
self.assertEqual(donate_option.split('/')[-1], v)
def test_insert_funding_yml_donation_links_with_corrupt_file(self):
os.chdir(self.testdir)
@@ -1736,8 +1739,9 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
)
)
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertIsNone(app.get(field))
self.assertEqual(app.get("Donate"), [])
self.assertIsNone(app.get("Liberapay"))
self.assertIsNone(app.get("OpenCollective"))
def test_sanitize_funding_yml(self):
with open(basedir / 'funding-usernames.yaml') as fp: