Repository: beeware/briefcase

Issue:
For Android, app requirements are not always updated when using `-r/--update-requirements`
### Describe the bug

The use of `-r/--update-requirements` should force the build of a project to install the app's requirements. This is currently true for all output formats except Android Gradle. When a Gradle project is built, the step to install requirements only runs the first time an app is built or if the `requirements.txt` file changes.

Therefore, running `briefcase build android -r` is not guaranteed to update the requirements in the Gradle build unless `requirements.txt` file generated from the app's requirements is different from the last time it ran.

### Steps to reproduce

1. Run `briefcase build android`
2. Run `briefcase build android -r` and see requirements are not installed again

### Expected behavior

The app's requirements are installed whenever the `-r/--update-requirements` flag is used. As noted in https://github.com/beeware/briefcase/pull/1719#issuecomment-2039631356, any change to the content of `requirements.txt` will trigger the requirements to be re-installed during the next Gradle build; so, simply adding a timestamp to the file would achieve this, for instance.

Furthermore, this installation should effectively eagerly update the app's requirements and their requirements and so forth. While `pip` can be configured to do this with `--upgrade-strategy=eager`, Briefcase normally achieves this by not updating an existing installation of requirements but by removing all installed requirements and installing everything again.

### Screenshots

_No response_

### Environment

- Operating System: pop os 22.04
- Python version: 3.12.2
- Software versions:
  - Briefcase: `0.3.18.dev162+g9b284776`

### Logs

`briefcase build android` - [briefcase.2024_04_05-11_46_02.build.log](https://github.com/beeware/briefcase/files/14887237/briefcase.2024_04_05-11_46_02.build.log)
`briefcase build android -r` - [briefcase.2024_04_05-11_46_50.build.log](https://github.com/beeware/briefcase/files/14887243/briefcase.2024_04_05-11_46_50.build.log)


### Additional context

_No response_

Discussion:

Patch:
diff --git a/changes/1721.bugfix.rst b/changes/1721.bugfix.rst
new file mode 100644
index 00000000..0db7c1dc
--- /dev/null
+++ b/changes/1721.bugfix.rst
@@ -0,0 +1,1 @@
+When using ``-r/--update-requirements`` for building for Android, the app's requirements are always reinstalled now.
diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py
index fd9e4031..3b4f5c82 100644
--- a/src/briefcase/commands/create.py
+++ b/src/briefcase/commands/create.py
@@ -6,7 +6,7 @@ import platform
 import shutil
 import subprocess
 import sys
-from datetime import date
+from datetime import date, datetime
 from pathlib import Path
 
 import briefcase
@@ -399,6 +399,9 @@ class CreateCommand(BaseCommand):
         with self.input.wait_bar("Writing requirements file..."):
             with requirements_path.open("w", encoding="utf-8") as f:
                 if requires:
+                    # Add timestamp so build systems (such as Gradle) detect a change
+                    # in the file and perform a re-installation of all requirements.
+                    f.write(f"# Generated {datetime.now()}\n")
                     for requirement in requires:
                         # If the requirement is a local path, convert it to
                         # absolute, because Flatpak moves the requirements file


Test patch:
diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py
index 59dda9dc..51ecf579 100644
--- a/tests/commands/create/test_install_app_requirements.py
+++ b/tests/commands/create/test_install_app_requirements.py
@@ -1,3 +1,4 @@
+import datetime
 import os
 import subprocess
 import sys
@@ -6,11 +7,14 @@ from unittest import mock
 import pytest
 import tomli_w
 
+import briefcase
 from briefcase.commands.create import _is_local_requirement
 from briefcase.console import LogLevel
 from briefcase.exceptions import BriefcaseCommandError, RequirementsInstallError
 from briefcase.integrations.subprocess import Subprocess
 
+GENERATED_DATETIME = "# Generated 2024-05-02 12:00:00.000500"
+
 
 @pytest.fixture
 def create_command(create_command, myapp):
@@ -19,6 +23,19 @@ def create_command(create_command, myapp):
     return create_command
 
 
+@pytest.fixture
+def mock_now(monkeypatch):
+    """Monkeypatch the ``datetime.now`` inside ``briefcase.commands.create``.
+
+    When this fixture is used, the datetime is locked to 2024 May 2 @ 12:00:00:000500.
+    """
+    now = datetime.datetime(2024, 5, 2, 12, 0, 0, 500)
+    datetime_mock = mock.MagicMock(wraps=datetime.datetime)
+    datetime_mock.now.return_value = now
+    monkeypatch.setattr(briefcase.commands.create, "datetime", datetime_mock)
+    return now
+
+
 def create_installation_artefacts(app_packages_path, packages):
     """Utility method for generating a function that will mock installation artefacts.
 
@@ -455,6 +472,7 @@ def test_app_requirements_requires(
     myapp,
     app_requirements_path,
     app_requirements_path_index,
+    mock_now,
 ):
     """If an app has an empty requirements list, a requirements file is still
     written."""
@@ -466,7 +484,7 @@ def test_app_requirements_requires(
     # requirements.txt doesn't exist either
     assert app_requirements_path.exists()
     with app_requirements_path.open(encoding="utf-8") as f:
-        assert f.read() == "first\nsecond==1.2.3\nthird>=3.2.1\n"
+        assert f.read() == f"{GENERATED_DATETIME}\nfirst\nsecond==1.2.3\nthird>=3.2.1\n"
 
     # Original app definitions haven't changed
     assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"]
@@ -499,7 +517,6 @@ def _test_app_requirements_paths(
     create_command,
     myapp,
     app_requirements_path,
-    app_requirements_path_index,
     tmp_path,
     requirement,
 ):
@@ -515,6 +532,7 @@ def _test_app_requirements_paths(
         assert f.read() == (
             "\n".join(
                 [
+                    GENERATED_DATETIME,
                     "first",
                     converted.format(tmp_path),
                     "third",
@@ -557,13 +575,13 @@ def test_app_requirements_non_paths(
     app_requirements_path_index,
     tmp_path,
     requirement,
+    mock_now,
 ):
     """Requirements which are not paths are left unchanged."""
     _test_app_requirements_paths(
         create_command,
         myapp,
         app_requirements_path,
-        app_requirements_path_index,
         tmp_path,
         requirement,
     )
@@ -588,13 +606,13 @@ def test_app_requirements_paths_unix(
     app_requirements_path_index,
     tmp_path,
     requirement,
+    mock_now,
 ):
     """Requirement paths in Unix format are expanded correctly."""
     _test_app_requirements_paths(
         create_command,
         myapp,
         app_requirements_path,
-        app_requirements_path_index,
         tmp_path,
         requirement,
     )
@@ -625,13 +643,13 @@ def test_app_requirements_paths_windows(
     app_requirements_path_index,
     tmp_path,
     requirement,
+    mock_now,
 ):
     """Requirement paths in Windows format are expanded correctly."""
     _test_app_requirements_paths(
         create_command,
         myapp,
         app_requirements_path,
-        app_requirements_path_index,
         tmp_path,
         requirement,
     )