diff --git a/fdroidserver/build.py b/fdroidserver/build.py index e5ee9663..6593ff0c 100644 --- a/fdroidserver/build.py +++ b/fdroidserver/build.py @@ -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) raise FDroidException() - common.set_FDroidPopen_env(build) + common.set_FDroidPopen_env(app, build) # create ..._toolsversion.log when running in builder vm if onserver: diff --git a/fdroidserver/common.py b/fdroidserver/common.py index ce5dc195..175c7f63 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -1201,6 +1201,25 @@ def get_src_tarball_name(appid, versionCode): 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): """Get the dir that this app will be built in.""" 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)) -def set_FDroidPopen_env(build=None): +def set_FDroidPopen_env(app=None, build=None): """Set up the environment variables for the build environment. 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 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 @@ -3230,6 +3253,8 @@ def set_FDroidPopen_env(build=None): if missinglocale: 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: path = build.ndk_path() paths = orig_path.split(os.pathsep) diff --git a/tests/test_build.py b/tests/test_build.py index e8e6927e..f7558c8c 100755 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -205,6 +205,7 @@ class BuildTest(unittest.TestCase): @mock.patch('fdroidserver.build.FDroidPopen') @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_source_date_epoch', lambda f: '1234567890') def test_build_local_maven(self, fake_FDroidPopen, fake_get_apk_id): """Test build_local() with a maven project""" @@ -330,6 +331,8 @@ class BuildTest(unittest.TestCase): 'fdroidserver.build.FDroidPopen', FakeProcess ) as _ignored, mock.patch( 'sdkmanager.install', wraps=fake_sdkmanager_install + ) as _ignored, mock.patch( + 'fdroidserver.common.get_source_date_epoch', lambda f: '1234567890' ) as _ignored: _ignored # silence the linters with self.assertRaises( @@ -378,6 +381,7 @@ class BuildTest(unittest.TestCase): @mock.patch('fdroidserver.build.FDroidPopen', FakeProcess) @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.get_source_date_epoch', lambda f: '1234567890') @mock.patch( 'fdroidserver.common.sha256sum', lambda f: 'ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e', @@ -453,6 +457,7 @@ class BuildTest(unittest.TestCase): self.assertTrue(ndk_dir.exists()) 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): """Test if `fdroid build` cleans ant and gradle build products""" os.chdir(self.testdir) diff --git a/tests/test_common.py b/tests/test_common.py index f6cdb0cb..521905aa 100755 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2468,7 +2468,7 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase): fdroidserver.common.fill_config_defaults(config) build = fdroidserver.metadata.Build() with self.assertRaises(TypeError): - fdroidserver.common.set_FDroidPopen_env(build) + fdroidserver.common.set_FDroidPopen_env(build=build) @mock.patch.dict(os.environ, clear=True) def test_ndk_paths_in_config_must_be_strings(self): @@ -2480,7 +2480,7 @@ class CommonTest(SetUpTearDownMixin, unittest.TestCase): build.ndk = 'r21d' os.environ['PATH'] = '/usr/bin:/usr/sbin' with self.assertRaises(TypeError): - fdroidserver.common.set_FDroidPopen_env(build) + fdroidserver.common.set_FDroidPopen_env(build=build) @mock.patch.dict(os.environ, clear=True) 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): 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)) 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): 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 = ( ( diff --git a/tests/test_scanner.py b/tests/test_scanner.py index 1dbe15b0..82f48d35 100755 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -350,6 +350,8 @@ class ScannerTest(unittest.TestCase): with mock.patch( 'fdroidserver.common.get_apk_id', return_value=(app.id, build.versionCode, build.versionName), + ), mock.patch( + 'fdroidserver.common.get_source_date_epoch', lambda f: '1234567890' ): with mock.patch( 'fdroidserver.common.is_debuggable_or_testOnly',