Repository: beeware/briefcase

Issue:
Modify build image of AppImage template to use Ubuntu 20.04
According to Github, by Dec 1 2022 they will [formally deprecate the Ubuntu-18.04 Actions runner](https://github.com/actions/runner-images/issues/6002). 

This puts Briefcase in an interesting situation. On the one hand Briefcase as used by end users is (mostly) unaffected - we encourage the use of Docker for building AppImages, and the Ubuntu 18.04 image will continue to exist. 

However, as a project, *we* have a problem. Due to historical issues using Docker in CI, we run our CI in `--no-docker` mode - which means that in December, we'll no longer be able to run CI for Linux. 

**Describe the solution you'd like**

Update the template and documentation to use Ubuntu 20.04 as the base image. 

This is something we've done before ([transitioning from 16.04 to 18.04 as the base image](https://github.com/beeware/briefcase-linux-appimage-template/commit/a172088d46f86b0a95799e8f03bc04149741c765). Interestingly, this was almost 2 years ago to the day, which matches the 2 year LTS release cadence of Ubuntu.

In addition to continued CI support, this has the added benefit that we'll have access to more recent packages in the base image.

**Describe alternatives you've considered**

1. Remove AppImages from our testing setup, but otherwise do nothing. Ubuntu 18.04 AppImage images will continue to work; they'll just be untested.
2. Modify CI to use Docker, but continue to use 18.04 as the base image. 
3. Deprecate the AppImage backend in favour of Flatpak.

**Additional context**

This may have some overlap with #824 - a switch of this magnitude requires some sort of control from a user perspective.

Comments:

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bddbba12..c0ad7731 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -131,27 +131,30 @@ jobs:
       run: |
         tox -e py
 
-  verify-desktop:
-    name: Desktop app verification
-    needs: smoke
+  verify-apps:
+    name: App verification
+    needs: platform-compat
     strategy:
       max-parallel: 4
       matrix:
-        platform: ['macos-latest', 'windows-latest', 'ubuntu-18.04']
+        os_name: ['macOS', 'windows', 'linux']
         framework: ['toga', 'pyside2', 'pyside6', 'ppb']
         include:
-        - platform: macos-latest
+        - os_name: macOS
+          platform: macos-latest
           briefcase-data-dir: ~/Library/Caches/org.beeware.briefcase
           pip-cache-dir: ~/Library/Caches/pip
           docker-cache-dir: ~/Library/Containers/com.docker.docker/Data/vms/0/
-        - platform: windows-latest
+        - os_name: windows
+          platform: windows-latest
           briefcase-data-dir: ~\AppData\Local\BeeWare\briefcase\Cache
           pip-cache-dir: ~\AppData\Local\pip\Cache
           docker-cache-dir: C:\ProgramData\DockerDesktop
-        - platform: ubuntu-18.04
+        - os_name: linux
+          # Need to use at least 22.04 to get the bugfix in flatpak for handling spaces in filenames.
+          platform: ubuntu-22.04
           briefcase-data-dir: ~/.cache/briefcase
           pip-cache-dir: ~/.cache/pip
-          briefcase-args: --no-docker
           # cache action cannot cache docker images (actions/cache#31)
           # docker-cache-dir: /var/lib/docker
     runs-on: ${{ matrix.platform }}
@@ -170,16 +173,57 @@ jobs:
       uses: actions/setup-python@v3.1.2
       with:
         python-version: ${{ env.python_version }}
-    - name: Install dependencies
-      if: matrix.platform == 'ubuntu-18.04'
+    - name: Install system dependencies
+      if: matrix.platform == 'ubuntu-22.04'
       run: |
-        sudo apt-get update -y
-        sudo apt-get install -y python3-gi python3-gi-cairo gir1.2-gtk-3.0 python3-dev libgirepository1.0-dev libcairo2-dev pkg-config
+        sudo apt-get install -y flatpak flatpak-builder
     - name: Install dependencies
       run: |
         pip install --upgrade pip
         pip install --upgrade setuptools
-        pip install tox
-    - name: Test
+        pip install .
+    - name: Create App
+      run: |
+        cd tests/apps
+        cat verify-${{ matrix.framework }}.config | briefcase new
+    - name: Build App
+      run: |
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create
+        briefcase build
+        briefcase package --adhoc-sign
+    - name: Build Xcode project
+      if: matrix.os_name == 'macOS'
+      run: |
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create ${{ matrix.os_name }} Xcode
+        briefcase build ${{ matrix.os_name }} Xcode
+        briefcase package ${{ matrix.os_name }} Xcode --adhoc-sign
+    - name: Build Visual Studio project
+      if: matrix.os_name == 'windows'
+      run: |
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create ${{ matrix.os_name }} VisualStudio
+        briefcase build ${{ matrix.os_name }} VisualStudio
+        briefcase package ${{ matrix.os_name }} VisualStudio --adhoc-sign
+    - name: Build Flatpak project
+      if: matrix.os_name == 'linux' && matrix.framework == 'toga'
+      run: |
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create ${{ matrix.os_name }} flatpak
+        briefcase build ${{ matrix.os_name }} flatpak
+        briefcase package ${{ matrix.os_name }} flatpak --adhoc-sign
+    - name: Build Android App
+      if: matrix.framework == 'toga'
+      run: |
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create android
+        briefcase build android
+        briefcase package android --adhoc-sign
+    - name: Build iOS App
+      if: matrix.platform == 'macos-latest' && matrix.framework == 'toga'
       run: |
-        tox -e verify-${{ matrix.framework }} -- ${{ matrix.briefcase-args }}
+        cd tests/apps/verify-${{ matrix.framework }}
+        briefcase create iOS
+        briefcase build iOS -d "iPhone SE (2nd generation)"
+        briefcase package iOS --adhoc-sign
diff --git a/changes/841.bugfix.rst b/changes/841.bugfix.rst
new file mode 100644
index 00000000..f05efe07
--- /dev/null
+++ b/changes/841.bugfix.rst
@@ -0,0 +1,1 @@
+When verifying the existence of the Android emulator, Briefcase now looks for the actual binary, not the folder that contains the binary. This was causing false positives on some Android SDK setups.
diff --git a/changes/844.misc.rst b/changes/844.misc.rst
new file mode 100644
index 00000000..7516c98d
--- /dev/null
+++ b/changes/844.misc.rst
@@ -0,0 +1,1 @@
+Linux tests are now run in Docker on CI; iOS, Android, Linux Flatpak, macOS Xcode, and Windows VisualStudio are tested.
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index ecc7193a..48dfe94d 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -406,7 +406,7 @@ connection.
         # might be missing.
         (self.root_path / "platforms").mkdir(exist_ok=True)
 
-        if (self.root_path / "emulator").exists():
+        if (self.emulator_path).exists():
             self.command.logger.debug("Android emulator is already installed.")
             return
 
diff --git a/tox.ini b/tox.ini
index e10d2c16..8682bb42 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = towncrier-check,docs,package,py{37,38,39,310,311},verify-{toga,pyside2,pyside6,ppb}
+envlist = towncrier-check,docs,package,py{37,38,39,310,311}
 skip_missing_interpreters = true
 
 [testenv]
@@ -55,17 +55,3 @@ passenv =
     TWINE_PASSWORD
 commands =
     python -m twine upload dist/*
-
-[testenv:verify-{toga,pyside2,pyside6,ppb}]
-setenv = PYTHONPATH = {toxinidir}/src
-changedir = {toxinidir}/tests/apps
-deps =
-allowlist_externals =
-    sh
-    rm
-commands =
-    rm -rf {envname}
-    sh -c 'cat {envname}.config | briefcase new'
-    sh -c 'cd {envname} && briefcase create {posargs}'
-    sh -c 'cd {envname} && briefcase build {posargs}'
-    sh -c 'cd {envname} && briefcase package --adhoc-sign {posargs}'


Test patch:
diff --git a/tests/integrations/android_sdk/ADB/test_command.py b/tests/integrations/android_sdk/ADB/test_command.py
index 07fdb370..cc5250ed 100644
--- a/tests/integrations/android_sdk/ADB/test_command.py
+++ b/tests/integrations/android_sdk/ADB/test_command.py
@@ -1,5 +1,6 @@
 import os
 import subprocess
+import sys
 from pathlib import Path
 
 import pytest
@@ -18,7 +19,12 @@ def test_simple_command(mock_sdk, tmp_path):
     # Check that adb was invoked with the expected commands
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(tmp_path / "sdk" / "platform-tools" / "adb"),
+            os.fsdecode(
+                tmp_path
+                / "sdk"
+                / "platform-tools"
+                / f"adb{'.exe' if sys.platform == 'win32' else ''}"
+            ),
             "-s",
             "exampleDevice",
             "example",
@@ -63,7 +69,12 @@ def test_error_handling(mock_sdk, tmp_path, name, exception):
     # Check that adb was invoked as expected
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(tmp_path / "sdk" / "platform-tools" / "adb"),
+            os.fsdecode(
+                tmp_path
+                / "sdk"
+                / "platform-tools"
+                / f"adb{'.exe' if sys.platform == 'win32' else ''}"
+            ),
             "-s",
             "exampleDevice",
             "example",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
index 610a8647..6315f404 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
@@ -1,16 +1,29 @@
 import os
 import subprocess
+import sys
 
 import pytest
 
 from briefcase.exceptions import BriefcaseCommandError
 
+from ....utils import create_file
+
+
+def create_emulator(root_path):
+    # Create `emulator` within `root_path`.
+    if sys.platform == "win32":
+        emulator_bin = "emulator.exe"
+    else:
+        emulator_bin = "emulator"
+
+    create_file(root_path / "emulator" / emulator_bin, "The Emulator", chmod=0o755)
+
 
 def test_succeeds_immediately_if_emulator_installed(mock_sdk):
     """`verify_emulator()` exits early if the emulator exists in its
     root_path."""
     # Create `emulator` within `root_path`.
-    (mock_sdk.root_path / "emulator").mkdir(parents=True)
+    create_emulator(mock_sdk.root_path)
 
     # Also create the platforms folder
     (mock_sdk.root_path / "platforms").mkdir(parents=True)
@@ -28,7 +41,7 @@ def test_succeeds_immediately_if_emulator_installed(mock_sdk):
 def test_creates_platforms_folder(mock_sdk):
     """If the platforms folder doesn't exist, it is created."""
     # Create `emulator` within `root_path`.
-    (mock_sdk.root_path / "emulator").mkdir(parents=True)
+    create_emulator(mock_sdk.root_path)
 
     # Verify the emulator. This should create the missing platforms folder.
     mock_sdk.verify_emulator()
@@ -59,6 +72,28 @@ def test_installs_android_emulator(mock_sdk):
     )
 
 
+def test_partial_android_emulator_install(mock_sdk):
+    """If the Android emulator is only partially installed, it's not
+    detected."""
+    # Create the emulator *directory*, but not the actual binary.
+    (mock_sdk.root_path / "emulator").mkdir(parents=True)
+
+    mock_sdk.verify_emulator()
+
+    # Platforms folder now exists
+    assert (mock_sdk.root_path / "platforms").exists()
+
+    mock_sdk.command.subprocess.run.assert_called_once_with(
+        [
+            os.fsdecode(mock_sdk.sdkmanager_path),
+            "platform-tools",
+            "emulator",
+        ],
+        env=mock_sdk.env,
+        check=True,
+    )
+
+
 def test_install_problems_are_reported(mock_sdk):
     """If the sdkmanager fails to properly install the Android emulator, an
     exception is raised."""
diff --git a/tests/integrations/android_sdk/conftest.py b/tests/integrations/android_sdk/conftest.py
index 1010a8a6..8845e05e 100644
--- a/tests/integrations/android_sdk/conftest.py
+++ b/tests/integrations/android_sdk/conftest.py
@@ -1,3 +1,4 @@
+import platform
 from pathlib import Path
 from unittest.mock import MagicMock
 
@@ -17,9 +18,8 @@ def mock_sdk(tmp_path):
     command.input = DummyConsole()
     command.logger = Log(verbosity=1)
 
-    # For default test purposes, assume we're on macOS x86_64
-    command.host_os = "Darwin"
-    command.host_arch = "x86_64"
+    command.host_arch = platform.machine()
+    command.host_os = platform.system()
 
     # Mock an empty environment
     command.os.environ = {}
diff --git a/tests/utils.py b/tests/utils.py
index 0295996a..165c13e1 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,3 +1,4 @@
+import os
 import zipfile
 from unittest.mock import MagicMock
 
@@ -31,7 +32,7 @@ class FsPathMock(MagicMock):
         return MagicMock(**kw)
 
 
-def create_file(filepath, content, mode="w"):
+def create_file(filepath, content, mode="w", chmod=None):
     """A test utility to create a file with known content.
 
     Ensures that the directory for the file exists, and writes a file with
@@ -48,6 +49,9 @@ def create_file(filepath, content, mode="w"):
     with filepath.open(mode) as f:
         f.write(content)
 
+    if chmod:
+        os.chmod(filepath, chmod)
+
     return filepath