SOURCE_DATE_EPOCH from app's git otherwise fdroiddata metadata file

https://reproducible-builds.org/docs/source-date-epoch
This commit is contained in:
Hans-Christoph Steiner 2025-03-31 11:44:00 +02:00
parent 0b6e304922
commit 20b36f1970
5 changed files with 84 additions and 5 deletions

View File

@ -479,7 +479,7 @@ def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, ext
logging.critical("Android NDK '%s' is not a directory!" % ndk_path) logging.critical("Android NDK '%s' is not a directory!" % ndk_path)
raise FDroidException() raise FDroidException()
common.set_FDroidPopen_env(build) common.set_FDroidPopen_env(app, build)
# create ..._toolsversion.log when running in builder vm # create ..._toolsversion.log when running in builder vm
if onserver: if onserver:

View File

@ -1201,6 +1201,25 @@ def get_src_tarball_name(appid, versionCode):
return f"{appid}_{versionCode}_src.tar.gz" return f"{appid}_{versionCode}_src.tar.gz"
def get_source_date_epoch(build_dir):
"""Return timestamp suitable for the SOURCE_DATE_EPOCH variable.
https://reproducible-builds.org/docs/source-date-epoch/
"""
try:
return git.repo.Repo(build_dir).git.log(n=1, pretty='%ct')
except Exception as e:
logging.warning('%s: %s', e.__class__.__name__, 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():
repo = git.repo.Repo(data_dir)
return repo.git.log('-n1', '--pretty=%ct', '--', metadata_file)
def get_build_dir(app): def get_build_dir(app):
"""Get the dir that this app will be built in.""" """Get the dir that this app will be built in."""
if app.RepoType == 'srclib': if app.RepoType == 'srclib':
@ -3202,12 +3221,16 @@ def remove_signing_keys(build_dir):
logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path)) logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
def set_FDroidPopen_env(build=None): def set_FDroidPopen_env(app=None, build=None):
"""Set up the environment variables for the build environment. """Set up the environment variables for the build environment.
There is only a weak standard, the variables used by gradle, so also set There is only a weak standard, the variables used by gradle, so also set
up the most commonly used environment variables for SDK and NDK. Also, if up the most commonly used environment variables for SDK and NDK. Also, if
there is no locale set, this will set the locale (e.g. LANG) to en_US.UTF-8. there is no locale set, this will set the locale (e.g. LANG) to en_US.UTF-8.
If an App instance is provided, then the SOURCE_DATE_EPOCH
environment variable will be set based on that app's source repo.
""" """
global env, orig_path global env, orig_path
@ -3230,6 +3253,8 @@ def set_FDroidPopen_env(build=None):
if missinglocale: if missinglocale:
env['LANG'] = 'en_US.UTF-8' env['LANG'] = 'en_US.UTF-8'
if app:
env['SOURCE_DATE_EPOCH'] = get_source_date_epoch(get_build_dir(app))
if build is not None: if build is not None:
path = build.ndk_path() path = build.ndk_path()
paths = orig_path.split(os.pathsep) paths = orig_path.split(os.pathsep)

View File

@ -205,6 +205,7 @@ class BuildTest(unittest.TestCase):
@mock.patch('fdroidserver.build.FDroidPopen') @mock.patch('fdroidserver.build.FDroidPopen')
@mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda f: False) @mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda f: False)
@mock.patch('fdroidserver.common.get_native_code', lambda f: 'x86') @mock.patch('fdroidserver.common.get_native_code', lambda f: 'x86')
@mock.patch('fdroidserver.common.get_source_date_epoch', lambda f: '1234567890')
def test_build_local_maven(self, fake_FDroidPopen, fake_get_apk_id): def test_build_local_maven(self, fake_FDroidPopen, fake_get_apk_id):
"""Test build_local() with a maven project""" """Test build_local() with a maven project"""
@ -330,6 +331,8 @@ class BuildTest(unittest.TestCase):
'fdroidserver.build.FDroidPopen', FakeProcess 'fdroidserver.build.FDroidPopen', FakeProcess
) as _ignored, mock.patch( ) as _ignored, mock.patch(
'sdkmanager.install', wraps=fake_sdkmanager_install 'sdkmanager.install', wraps=fake_sdkmanager_install
) as _ignored, mock.patch(
'fdroidserver.common.get_source_date_epoch', lambda f: '1234567890'
) as _ignored: ) as _ignored:
_ignored # silence the linters _ignored # silence the linters
with self.assertRaises( with self.assertRaises(
@ -378,6 +381,7 @@ class BuildTest(unittest.TestCase):
@mock.patch('fdroidserver.build.FDroidPopen', FakeProcess) @mock.patch('fdroidserver.build.FDroidPopen', FakeProcess)
@mock.patch('fdroidserver.common.get_native_code', lambda _ignored: 'x86') @mock.patch('fdroidserver.common.get_native_code', lambda _ignored: 'x86')
@mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda _ignored: False) @mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda _ignored: False)
@mock.patch('fdroidserver.common.get_source_date_epoch', lambda f: '1234567890')
@mock.patch( @mock.patch(
'fdroidserver.common.sha256sum', 'fdroidserver.common.sha256sum',
lambda f: 'ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e', lambda f: 'ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e',
@ -453,6 +457,7 @@ class BuildTest(unittest.TestCase):
self.assertTrue(ndk_dir.exists()) self.assertTrue(ndk_dir.exists())
self.assertTrue(os.path.exists(config['ndk_paths'][ndk_version])) self.assertTrue(os.path.exists(config['ndk_paths'][ndk_version]))
@mock.patch('fdroidserver.common.get_source_date_epoch', lambda f: '1234567890')
def test_build_local_clean(self): def test_build_local_clean(self):
"""Test if `fdroid build` cleans ant and gradle build products""" """Test if `fdroid build` cleans ant and gradle build products"""
os.chdir(self.testdir) os.chdir(self.testdir)

View File

@ -2468,7 +2468,7 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
build = fdroidserver.metadata.Build() build = fdroidserver.metadata.Build()
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
fdroidserver.common.set_FDroidPopen_env(build) fdroidserver.common.set_FDroidPopen_env(build=build)
@mock.patch.dict(os.environ, clear=True) @mock.patch.dict(os.environ, clear=True)
def test_ndk_paths_in_config_must_be_strings(self): def test_ndk_paths_in_config_must_be_strings(self):
@ -2480,7 +2480,7 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase):
build.ndk = 'r21d' build.ndk = 'r21d'
os.environ['PATH'] = '/usr/bin:/usr/sbin' os.environ['PATH'] = '/usr/bin:/usr/sbin'
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
fdroidserver.common.set_FDroidPopen_env(build) fdroidserver.common.set_FDroidPopen_env(build=build)
@mock.patch.dict(os.environ, clear=True) @mock.patch.dict(os.environ, clear=True)
def test_FDroidPopen_envs_paths_can_be_pathlib(self): def test_FDroidPopen_envs_paths_can_be_pathlib(self):
@ -2567,7 +2567,7 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase):
with mock.patch.dict(os.environ, clear=True): with mock.patch.dict(os.environ, clear=True):
os.environ['PATH'] = '/usr/bin:/usr/sbin' os.environ['PATH'] = '/usr/bin:/usr/sbin'
fdroidserver.common.set_FDroidPopen_env(build) fdroidserver.common.set_FDroidPopen_env(build=build)
self.assertNotIn('', os.getenv('PATH').split(os.pathsep)) self.assertNotIn('', os.getenv('PATH').split(os.pathsep))
def test_is_repo_file(self): def test_is_repo_file(self):
@ -2993,6 +2993,53 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase):
for mirror in fdroidserver.common.append_filename_to_mirrors(filename, mirrors): for mirror in fdroidserver.common.append_filename_to_mirrors(filename, mirrors):
self.assertTrue(mirror['url'].endswith('/' + filename)) self.assertTrue(mirror['url'].endswith('/' + filename))
def test_get_source_date_epoch(self):
git_repo = git.Repo.init(self.testdir)
Path('README').write_text('file to commit')
git_repo.git.add(all=True)
git_repo.index.commit("README")
self.assertEqual(
git_repo.git.log(n=1, pretty='%ct'),
fdroidserver.common.get_source_date_epoch(self.testdir),
)
def test_get_source_date_epoch_no_scm(self):
self.assertIsNone(fdroidserver.common.get_source_date_epoch(self.testdir))
def test_get_source_date_epoch_not_git(self):
"""Test when build_dir is not a git repo, e.g. hg, svn, etc."""
appid = 'com.example'
build_dir = Path(self.testdir) / 'build' / appid
fdroiddata = build_dir.parent.parent
(fdroiddata / 'metadata').mkdir()
build_dir.mkdir(parents=True)
os.chdir(build_dir)
git_repo = git.Repo.init(fdroiddata) # fdroiddata is always a git repo
with (fdroiddata / f'metadata/{appid}.yml').open('w') as fp:
fp.write('AutoName: Example App\n')
git_repo.git.add(all=True)
git_repo.index.commit("update README")
self.assertEqual(
git.repo.Repo(fdroiddata).git.log(n=1, pretty='%ct'),
fdroidserver.common.get_source_date_epoch(build_dir),
)
@mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True)
def test_set_FDroidPopen_env_with_app(self):
"""Test SOURCE_DATE_EPOCH in FDroidPopen when build_dir is a git repo."""
os.chdir(self.testdir)
app = fdroidserver.metadata.App()
app.id = 'com.example'
build_dir = Path(self.testdir) / 'build' / app.id
git_repo = git.Repo.init(build_dir)
Path('README').write_text('file to commit')
git_repo.git.add(all=True)
now = datetime.now(timezone.utc)
git_repo.index.commit("README", commit_date=now)
fdroidserver.common.set_FDroidPopen_env(app)
p = fdroidserver.common.FDroidPopen(['printenv', 'SOURCE_DATE_EPOCH'])
self.assertEqual(int(p.output), int(now.timestamp()))
APKS_WITH_JAR_SIGNATURES = ( APKS_WITH_JAR_SIGNATURES = (
( (

View File

@ -350,6 +350,8 @@ class ScannerTest(unittest.TestCase):
with mock.patch( with mock.patch(
'fdroidserver.common.get_apk_id', 'fdroidserver.common.get_apk_id',
return_value=(app.id, build.versionCode, build.versionName), return_value=(app.id, build.versionCode, build.versionName),
), mock.patch(
'fdroidserver.common.get_source_date_epoch', lambda f: '1234567890'
): ):
with mock.patch( with mock.patch(
'fdroidserver.common.is_debuggable_or_testOnly', 'fdroidserver.common.is_debuggable_or_testOnly',