From b00b9c93904f4fc4d86f9f3a8290e4010515cd5f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 9 Oct 2025 17:00:58 +0200 Subject: [PATCH] metadata: get_single_build() for new subcommands The new subcommands operate on a single APPID:VERSIONCODE pair, so this new function fetches the metadata needed for that operation, and includes any required checks. The algorithm came from: uniqx/fdroidserver@2b779e65992f1ca8b79d4e68957afccde28d0e07 This reworking of it includes: * removed `check_disabled`, seems like the scheduler should handle that? If it is required, it can be added later. * standard variable names * fixed spelling errors and hopefully improved docstrings * black format * added tests * standardized the strings, made them localizable --- fdroidserver/common.py | 10 +++++--- fdroidserver/metadata.py | 51 +++++++++++++++++++++++++++++++++++++++- tests/test_metadata.py | 51 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 7aada1e8..e3062129 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -1236,6 +1236,10 @@ def get_src_tarball_name(appid, versionCode): return f"{appid}_{versionCode}_src.tar.gz" +def get_metadatapath(appid): + return f'metadata/{appid}.yml' + + def get_source_date_epoch(build_dir): """Return timestamp suitable for the SOURCE_DATE_EPOCH variable. @@ -1249,10 +1253,10 @@ def get_source_date_epoch(build_dir): build_dir = Path(build_dir) appid = build_dir.name data_dir = build_dir.parent.parent - metadata_file = f'metadata/{appid}.yml' - if (data_dir / '.git').exists() and (data_dir / metadata_file).exists(): + metadatapath = get_metadatapath(appid) + if (data_dir / '.git').exists() and (data_dir / metadatapath).exists(): repo = git.repo.Repo(data_dir) - return repo.git.log('-n1', '--pretty=%ct', '--', metadata_file) + return repo.git.log('-n1', '--pretty=%ct', '--', metadatapath) def get_build_dir(app): diff --git a/fdroidserver/metadata.py b/fdroidserver/metadata.py index 0d9195be..c9c592ca 100644 --- a/fdroidserver/metadata.py +++ b/fdroidserver/metadata.py @@ -3,7 +3,8 @@ # metadata.py - part of the FDroid server tools # Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí -# Copyright (C) 2017-2018 Michael Pöhn +# Copyright (C) 2014-2025 Hans-Christoph Steiner +# Copyright (C) 2017-2025 Michael Pöhn # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -599,6 +600,54 @@ def read_metadata(appid_to_vercode={}, sort_by_time=False): return apps +def get_single_build(appid, versionCode): + """Read 1 single metadata file from the file system + 1 singled out build. + + Parameters + ---------- + appid + Application ID app to be loaded (e.g. 'org.fdroid.fdroid') + versionCode + Android versionCode of the build to be returned + + Returns + ------- + Tuple + A tuple of (app, build) dictionaries, containing the paired data from + the metadata file. + + Raises + ------ + MetaDataException + If parsing or selecting the requested version fails. + """ + metadatapath = common.get_metadatapath(appid) + apps = read_metadata({appid: versionCode}) + if len(apps) != 1 or appid not in apps: + raise MetaDataException( + _("Could not read {path} for {appid_versionCode}").format( + path=metadatapath, appid_versionCode=f"{appid}:{versionCode}" + ) + ) + builds = apps[appid].get("Builds") + if not builds: + raise MetaDataException( + _("{path} has no '{fieldname}'").format( + path=metadatapath, fieldname='Builds:' + ) + ) + for build in builds: + if build.get('versionCode') == versionCode: + return apps[appid], build + raise MetaDataException( + _("Did not find '{versionCode}' in '{fieldname}' of {path}").format( + versionCode=f"versionCode: {versionCode}", + fieldname='Builds:', + path=metadatapath, + ) + ) + + def parse_metadata(metadatapath): """Parse metadata file, also checking the source repo for .fdroid.yml. diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 84040024..85ebb0de 100755 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -18,7 +18,7 @@ import fdroidserver from fdroidserver import metadata from fdroidserver._yaml import yaml from fdroidserver.common import DEFAULT_LOCALE -from fdroidserver.exception import MetaDataException +from fdroidserver.exception import FDroidException, MetaDataException from .shared_test_code import TmpCwd, mkdtemp @@ -233,6 +233,55 @@ class MetadataTest(unittest.TestCase): logging_error.assert_called() self.assertEqual(3, len(logging_error.call_args_list)) + def test_get_single_build(self): + """Test if this can successfully return an app and a build""" + os.chdir(self.testdir) + appid = 'one.build' + versionCode = 1234567890 + metadatapath = Path(fdroidserver.common.get_metadatapath(appid)) + metadatapath.parent.mkdir() + metadatapath.write_text(f'Name: One\nBuilds:\n - versionCode: {versionCode}\n') + app, build = metadata.get_single_build(appid, versionCode) + self.assertEqual(app.id, appid) + self.assertEqual(build.versionCode, versionCode) + + def test_get_single_build_no_metadatapath(self): + """Test if the right error is thrown if no matching metadatapath found.""" + os.chdir(self.testdir) + appid = 'does.not.exist' + versionCode = 1234567890 + with self.assertLogs(level='DEBUG') as logs: + with self.assertRaises(FDroidException): + metadata.get_single_build(appid, versionCode) + self.assertIn(appid, logs.output[0]) + self.assertNotIn(str(versionCode), logs.output[0]) + + def test_get_single_build_no_builds(self): + """Test if the right error is thrown if Builds: field found.""" + os.chdir(self.testdir) + appid = 'no.builds' + versionCode = 1234567890 + metadatapath = Path(fdroidserver.common.get_metadatapath(appid)) + metadatapath.parent.mkdir() + metadatapath.write_text('Name: No Builds\n') + with self.assertRaises(MetaDataException) as e: + metadata.get_single_build(appid, versionCode) + self.assertIn(appid, e.exception.value) + self.assertNotIn(str(versionCode), e.exception.value) + + def test_get_single_build_no_match(self): + """Test if the right error is thrown if Builds: field found.""" + os.chdir(self.testdir) + appid = 'no.builds' + versionCode = 1234567890 + metadatapath = Path(fdroidserver.common.get_metadatapath(appid)) + metadatapath.parent.mkdir() + metadatapath.write_text('Name: No Builds\nBuilds: [{versionCode: 0}]\n') + with self.assertRaises(MetaDataException) as e: + metadata.get_single_build(appid, versionCode) + self.assertIn(appid, e.exception.value) + self.assertIn(str(versionCode), e.exception.value) + @mock.patch('git.Repo', mock.Mock()) def test_metadata_overrides_dot_fdroid_yml(self): """Fields in metadata files should override anything in .fdroid.yml."""