Repository: beeware/briefcase

Issue:
Catch exceptions when deleting JDK while it's running on Windows
### Describe the bug

If you run `briefcase upgrade` while java is running, Briefcase crashes out.

```
❯ briefcase upgrade -vv
Using JDK at C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\java17
Using Android SDK at C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk

[upgrade] Briefcase will upgrade the following tools:
 - Android SDK (android_sdk)
 - Java JDK (java)
 - RCEdit (rcedit)

[android_sdk] Upgrading Android SDK...

>>> Running Command:
>>>     'C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk\cmdline-tools\9.0\bin\sdkmanager.bat' --update
>>> Working Directory:
>>>     C:\Users\user\github\beeware\briefcase\tmp\helloworld
>>> Environment Overrides:
>>>     ANDROID_HOME=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk
>>>     ANDROID_SDK_ROOT=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk
>>>     JAVA_HOME=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\java17
[===                                    ] 10% Computing updates...
No updates available
[=======================================] 100% Computing updates...

>>> Return code: 0

[java] Upgrading Java JDK...
Removing old JDK install...

Log saved to C:\Users\user\github\beeware\briefcase\tmp\helloworld\logs\briefcase.2023_11_01-14_06_04.upgrade.log

Traceback (most recent call last):
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\briefcase.exe\__main__.py", line 7, in <module>
  File "C:\Users\user\github\beeware\briefcase\src\briefcase\__main__.py", line 25, in main
    command(**options)
  File "C:\Users\user\github\beeware\briefcase\src\briefcase\commands\upgrade.py", line 114, in __call__
    tool.upgrade()
  File "C:\Users\user\github\beeware\briefcase\src\briefcase\integrations\base.py", line 137, in upgrade
    self.uninstall()
  File "C:\Users\user\github\beeware\briefcase\src\briefcase\integrations\java.py", line 319, in uninstall
    self.tools.shutil.rmtree(self.java_home)
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py", line 750, in rmtree
    return _rmtree_unsafe(path, onerror)
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py", line 615, in _rmtree_unsafe
    _rmtree_unsafe(fullname, onerror)
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py", line 620, in _rmtree_unsafe
    onerror(os.unlink, fullname, sys.exc_info())
  File "C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py", line 618, in _rmtree_unsafe
    os.unlink(fullname)
PermissionError: [WinError 5] Access is denied: 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin\\java.dll'
```

### Steps to reproduce

1. Run `briefcase build android`
2. Run `briefcase upgrade`

### Expected behavior

The exception is caught and a more meaningful error is presented to the user.

### Screenshots

_No response_

### Environment

- Operating System: Windows 11
- Python version: 3.10.11
- Software versions:
  - Briefcase: `0.3.17.dev44+g0b10614f`

### Logs

<details>
<summary>Log</summary>

```
Date/Time:       2023-11-01 14:06:05 
Command line:    C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\briefcase upgrade -vv

OS Release:      Windows 10
OS Version:      10.0.22621
Architecture:    AMD64
Platform:        Windows-10-10.0.22621-SP0

Python exe:      C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts\python.exe
Python version:  3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Virtual env:     True
Conda env:       False

Briefcase:       0.3.17.dev44+g0b10614f
Target platform: windows
Target format:   None

Environment Variables:
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Users\user\AppData\Roaming
    COMMONPROGRAMFILES=C:\Program Files\Common Files
    COMMONPROGRAMFILES(X86)=C:\Program Files (x86)\Common Files
    COMMONPROGRAMW6432=C:\Program Files\Common Files
    COMPUTERNAME=VM-WIN11
    COMSPEC=C:\Windows\system32\cmd.exe
    DRIVERDATA=C:\Windows\System32\Drivers\DriverData
    HOMEDRIVE=C:
    HOMEPATH=\Users\user
    LOCALAPPDATA=C:\Users\user\AppData\Local
    LOGONSERVER=\\VM-WIN11
    NUMBER_OF_PROCESSORS=4
    ONEDRIVE=C:\Users\user\OneDrive
    OS=Windows_NT
    PATH=C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase\Scripts;C:\Program Files\PowerShell\7;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\.pyenv\pyenv-win\versions\3.9.13;C:\Users\user\.pyenv\pyenv-win\versions\3.8.10;C:\Users\user\.pyenv\pyenv-win\versions\3.10.11;C:\Users\user\.pyenv\pyenv-win\versions\3.11.3;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Git\cmd;;C:\Program Files\dotnet\;C:\Program Files\starship\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\Program Files\PowerShell\7\;C:\Users\user\.pyenv\pyenv-win\bin;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\AppData\Local\Microsoft\WindowsApps;C:\Users\user\.dotnet\tools;C:\Users\user\.dotnet\tools;C:\Users\user\AppData\Local\Programs\Microsoft VS Code\bin
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL
    POWERSHELL_DISTRIBUTION_CHANNEL=MSI:Windows 10 Pro
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=AMD64 Family 25 Model 33 Stepping 0, AuthenticAMD
    PROCESSOR_LEVEL=25
    PROCESSOR_REVISION=2100
    PROGRAMDATA=C:\ProgramData
    PROGRAMFILES=C:\Program Files
    PROGRAMFILES(X86)=C:\Program Files (x86)
    PROGRAMW6432=C:\Program Files
    PSMODULEPATH=C:\Users\user\Documents\PowerShell\Modules;C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    PUBLIC=C:\Users\Public
    PYENV=C:\Users\user\.pyenv\pyenv-win\
    PYENV_HOME=C:\Users\user\.pyenv\pyenv-win\
    PYENV_ROOT=C:\Users\user\.pyenv\pyenv-win\
    SESSIONNAME=Console
    STARSHIP_SESSION_KEY=********************
    STARSHIP_SHELL=pwsh
    SYSTEMDRIVE=C:
    SYSTEMROOT=C:\Windows
    TEMP=C:\Users\user\AppData\Local\Temp
    TMP=C:\Users\user\AppData\Local\Temp
    USERDOMAIN=vm-win11
    USERDOMAIN_ROAMINGPROFILE=vm-win11
    USERNAME=user
    USERPROFILE=C:\Users\user
    VIRTUAL_ENV=C:\Users\user\github\beeware\briefcase\venv-3.10-briefcase
    VIRTUAL_ENV_DISABLE_PROMPT=1
    WINDIR=C:\Windows
    WSLENV=WT_SESSION:WT_PROFILE_ID:
    WT_PROFILE_ID={574e775e-4f2a-5b96-ac1e-a2962a402336}
    WT_SESSION=b34cf4e9-ae43-4a8a-9fa7-e6157e73aac3
    _OLD_VIRTUAL_PATH=C:\Program Files\PowerShell\7;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\.pyenv\pyenv-win\versions\3.9.13;C:\Users\user\.pyenv\pyenv-win\versions\3.8.10;C:\Users\user\.pyenv\pyenv-win\versions\3.10.11;C:\Users\user\.pyenv\pyenv-win\versions\3.11.3;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Git\cmd;;C:\Program Files\dotnet\;C:\Program Files\starship\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\Program Files\PowerShell\7\;C:\Users\user\.pyenv\pyenv-win\bin;C:\Users\user\.pyenv\pyenv-win\shims;C:\Users\user\AppData\Local\Microsoft\WindowsApps;C:\Users\user\.dotnet\tools;C:\Users\user\.dotnet\tools;C:\Users\user\AppData\Local\Programs\Microsoft VS Code\bin

Briefcase Log:
[14:06:03] Using JDK at C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\java17                                                                                 java.py:261
           Using Android SDK at C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk                                                             android_sdk.py:364
                                                                                                                                                                      upgrade.py:104
           [upgrade] Briefcase will upgrade the following tools:                                                                                                      upgrade.py:104
            - Android SDK (android_sdk)                                                                                                                               upgrade.py:109
            - Java JDK (java)                                                                                                                                         upgrade.py:109
            - RCEdit (rcedit)                                                                                                                                         upgrade.py:109
                                                                                                                                                                      upgrade.py:113
           [android_sdk] Upgrading Android SDK...                                                                                                                     upgrade.py:113
                                                                                                                                                                   subprocess.py:711
           >>> Running Command:                                                                                                                                    subprocess.py:711
           >>>     'C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk\cmdline-tools\9.0\bin\sdkmanager.bat' --update                           subprocess.py:711
           >>> Working Directory:                                                                                                                                  subprocess.py:711
           >>>     C:\Users\user\github\beeware\briefcase\tmp\helloworld                                                                                           subprocess.py:711
           >>> Environment Overrides:                                                                                                                              subprocess.py:711
           >>>     ANDROID_HOME=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk                                                              subprocess.py:711
           >>>     ANDROID_SDK_ROOT=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk                                                          subprocess.py:711
           >>>     JAVA_HOME=C:\Users\user\AppData\Local\BeeWare\briefcase\Cache\tools\java17                                                                      subprocess.py:711
[14:06:04] >>> Return code: 0                                                                                                                                      subprocess.py:711
                                                                                                                                                                      upgrade.py:113
           [java] Upgrading Java JDK...                                                                                                                               upgrade.py:113
           Removing old JDK install...                                                                                                                                   java.py:315

Main thread traceback:
╭─────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ────────────────────────────────────────────────────────────────────────╮
│ C:\Users\user\github\beeware\briefcase\src\briefcase\__main__.py:25 in main                                                                                                      │
│                                                                                                                                                                                  │
│   22 │   │   command = Command(logger=logger, console=console)                                                                                                                   │
│   23 │   │   options = command.parse_options(extra=extra_cmdline)                                                                                                                │
│   24 │   │   command.parse_config(Path.cwd() / "pyproject.toml")                                                                                                                 │
│ ❱ 25 │   │   command(**options)                                                                                                                                                  │
│   26 │   except HelpText as e:                                                                                                                                                   │
│   27 │   │   logger.info()                                                                                                                                                       │
│   28 │   │   logger.info(str(e))                                                                                                                                                 │
│                                                                                                                                                                                  │
│ ╭───────────────────────────────────────── locals ─────────────────────────────────────────╮                                                                                     │
│ │       command = <briefcase.commands.upgrade.UpgradeCommand object at 0x000002D701B8D600> │                                                                                     │
│ │       Command = <class 'briefcase.commands.upgrade.UpgradeCommand'>                      │                                                                                     │
│ │       console = <briefcase.console.Console object at 0x000002D77EFFC0A0>                 │                                                                                     │
│ │ extra_cmdline = ['-vv']                                                                  │                                                                                     │
│ │        logger = <briefcase.console.Log object at 0x000002D77F040B50>                     │                                                                                     │
│ │       options = {'list_tools': False, 'tool_list': []}                                   │                                                                                     │
│ │        result = 0                                                                        │                                                                                     │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯                                                                                     │
│                                                                                                                                                                                  │
│ C:\Users\user\github\beeware\briefcase\src\briefcase\commands\upgrade.py:114 in __call__                                                                                         │
│                                                                                                                                                                                  │
│   111 │   │   │   if not list_tools:                                                                                                                                             │
│   112 │   │   │   │   for tool in tools_to_upgrade:                                                                                                                              │
│   113 │   │   │   │   │   self.logger.info(f"Upgrading {tool.full_name}...", prefix=tool.name)                                                                                   │
│ ❱ 114 │   │   │   │   │   tool.upgrade()                                                                                                                                         │
│   115 │   │   else:                                                                                                                                                              │
│   116 │   │   │   self.logger.info("Briefcase is not managing any tools.")                                                                                                       │
│   117                                                                                                                                                                            │
│                                                                                                                                                                                  │
│ ╭─────────────────────────────────────────────── locals ───────────────────────────────────────────────╮                                                                         │
│ │       list_tools = False                                                                             │                                                                         │
│ │          options = {}                                                                                │                                                                         │
│ │             self = <briefcase.commands.upgrade.UpgradeCommand object at 0x000002D701B8D600>          │                                                                         │
│ │             tool = <briefcase.integrations.java.JDK object at 0x000002D77EB9CD00>                    │                                                                         │
│ │        tool_list = []                                                                                │                                                                         │
│ │ tools_to_upgrade = [                                                                                 │                                                                         │
│ │                    │   <briefcase.integrations.android_sdk.AndroidSDK object at 0x000002D702A53E80>, │                                                                         │
│ │                    │   <briefcase.integrations.java.JDK object at 0x000002D77EB9CD00>,               │                                                                         │
│ │                    │   <briefcase.integrations.rcedit.RCEdit object at 0x000002D702A50DC0>           │                                                                         │
│ │                    ]                                                                                 │                                                                         │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                                         │
│                                                                                                                                                                                  │
│ C:\Users\user\github\beeware\briefcase\src\briefcase\integrations\base.py:137 in upgrade                                                                                         │
│                                                                                                                                                                                  │
│   134 │   │   if self.managed_install:                                                         ╭─────────────────────────────── locals ────────────────────────────────╮         │
│   135 │   │   │   if not self.exists():                                                        │ self = <briefcase.integrations.java.JDK object at 0x000002D77EB9CD00> │         │
│   136 │   │   │   │   raise MissingToolError(self.full_name)                                   ╰───────────────────────────────────────────────────────────────────────╯         │
│ ❱ 137 │   │   │   self.uninstall()                                                                                                                                               │
│   138 │   │   │   self.install()                                                                                                                                                 │
│   139 │   │   else:                                                                                                                                                              │
│   140 │   │   │   raise NonManagedToolError(self.full_name)                                                                                                                      │
│                                                                                                                                                                                  │
│ C:\Users\user\github\beeware\briefcase\src\briefcase\integrations\java.py:319 in uninstall                                                                                       │
│                                                                                                                                                                                  │
│   316 │   │   │   if self.tools.host_os == "Darwin":                                           ╭─────────────────────────────── locals ────────────────────────────────╮         │
│   317 │   │   │   │   self.tools.shutil.rmtree(self.java_home.parent.parent)                   │ self = <briefcase.integrations.java.JDK object at 0x000002D77EB9CD00> │         │
│   318 │   │   │   else:                                                                        ╰───────────────────────────────────────────────────────────────────────╯         │
│ ❱ 319 │   │   │   │   self.tools.shutil.rmtree(self.java_home)                                                                                                                   │
│   320                                                                                                                                                                            │
│                                                                                                                                                                                  │
│ C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py:750 in rmtree                                                                                                      │
│                                                                                                                                                                                  │
│    747 │   │   │   onerror(os.path.islink, path, sys.exc_info())                                                                                                                 │
│    748 │   │   │   # can't continue even if onerror hook returns                                                                                                                 │
│    749 │   │   │   return                                                                                                                                                        │
│ ❱  750 │   │   return _rmtree_unsafe(path, onerror)                                                                                                                              │
│    751                                                                                                                                                                           │
│    752 # Allow introspection of whether or not the hardening against symlink                                                                                                     │
│    753 # attacks is supported on the current platform                                                                                                                            │
│                                                                                                                                                                                  │
│ ╭──────────────────────────────────────────── locals ─────────────────────────────────────────────╮                                                                              │
│ │ ignore_errors = False                                                                           │                                                                              │
│ │       onerror = <function rmtree.<locals>.onerror at 0x000002D702B0B0A0>                        │                                                                              │
│ │          path = WindowsPath('C:/Users/user/AppData/Local/BeeWare/briefcase/Cache/tools/java17') │                                                                              │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯                                                                              │
│                                                                                                                                                                                  │
│ C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py:615 in _rmtree_unsafe                                                                                              │
│                                                                                                                                                                                  │
│    612 │   │   │   except OSError:                                                                                                                                               │
│    613 │   │   │   │   onerror(os.path.islink, fullname, sys.exc_info())                                                                                                         │
│    614 │   │   │   │   continue                                                                                                                                                  │
│ ❱  615 │   │   │   _rmtree_unsafe(fullname, onerror)                                                                                                                             │
│    616 │   │   else:                                                                                                                                                             │
│    617 │   │   │   try:                                                                                                                                                          │
│    618 │   │   │   │   os.unlink(fullname)                                                                                                                                       │
│                                                                                                                                                                                  │
│ ╭─────────────────────────────────────────── locals ────────────────────────────────────────────╮                                                                                │
│ │    entries = [                                                                                │                                                                                │
│ │              │   <DirEntry 'bin'>,                                                            │                                                                                │
│ │              │   <DirEntry 'conf'>,                                                           │                                                                                │
│ │              │   <DirEntry 'include'>,                                                        │                                                                                │
│ │              │   <DirEntry 'jmods'>,                                                          │                                                                                │
│ │              │   <DirEntry 'legal'>,                                                          │                                                                                │
│ │              │   <DirEntry 'lib'>,                                                            │                                                                                │
│ │              │   <DirEntry 'NOTICE'>,                                                         │                                                                                │
│ │              │   <DirEntry 'release'>                                                         │                                                                                │
│ │              ]                                                                                │                                                                                │
│ │      entry = <DirEntry 'bin'>                                                                 │                                                                                │
│ │   fullname = 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin' │                                                                                │
│ │    onerror = <function rmtree.<locals>.onerror at 0x000002D702B0B0A0>                         │                                                                                │
│ │       path = WindowsPath('C:/Users/user/AppData/Local/BeeWare/briefcase/Cache/tools/java17')  │                                                                                │
│ │ scandir_it = <nt.ScandirIterator object at 0x000002D7020F5E20>                                │                                                                                │
│ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯                                                                                │
│                                                                                                                                                                                  │
│ C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py:620 in _rmtree_unsafe                                                                                              │
│                                                                                                                                                                                  │
│    617 │   │   │   try:                                                                                                                                                          │
│    618 │   │   │   │   os.unlink(fullname)                                                                                                                                       │
│    619 │   │   │   except OSError:                                                                                                                                               │
│ ❱  620 │   │   │   │   onerror(os.unlink, fullname, sys.exc_info())                                                                                                              │
│    621 │   try:                                                                                                                                                                  │
│    622 │   │   os.rmdir(path)                                                                                                                                                    │
│    623 │   except OSError:                                                                                                                                                       │
│                                                                                                                                                                                  │
│ ╭──────────────────────────────────────────────── locals ─────────────────────────────────────────────────╮                                                                      │
│ │    entries = [                                                                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-console-l1-1-0.dll'>,                                       │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-datetime-l1-1-0.dll'>,                                      │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-debug-l1-1-0.dll'>,                                         │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-errorhandling-l1-1-0.dll'>,                                 │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l1-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l1-2-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l2-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-handle-l1-1-0.dll'>,                                        │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-heap-l1-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-interlocked-l1-1-0.dll'>,                                   │                                                                      │
│ │              │   ... +110                                                                               │                                                                      │
│ │              ]                                                                                          │                                                                      │
│ │      entry = <DirEntry 'java.dll'>                                                                      │                                                                      │
│ │   fullname = 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin\\java.dll' │                                                                      │
│ │    onerror = <function rmtree.<locals>.onerror at 0x000002D702B0B0A0>                                   │                                                                      │
│ │       path = 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin'           │                                                                      │
│ │ scandir_it = <nt.ScandirIterator object at 0x000002D7020F6920>                                          │                                                                      │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                                      │
│                                                                                                                                                                                  │
│ C:\Users\user\.pyenv\pyenv-win\versions\3.10.11\lib\shutil.py:618 in _rmtree_unsafe                                                                                              │
│                                                                                                                                                                                  │
│    615 │   │   │   _rmtree_unsafe(fullname, onerror)                                                                                                                             │
│    616 │   │   else:                                                                                                                                                             │
│    617 │   │   │   try:                                                                                                                                                          │
│ ❱  618 │   │   │   │   os.unlink(fullname)                                                                                                                                       │
│    619 │   │   │   except OSError:                                                                                                                                               │
│    620 │   │   │   │   onerror(os.unlink, fullname, sys.exc_info())                                                                                                              │
│    621 │   try:                                                                                                                                                                  │
│                                                                                                                                                                                  │
│ ╭──────────────────────────────────────────────── locals ─────────────────────────────────────────────────╮                                                                      │
│ │    entries = [                                                                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-console-l1-1-0.dll'>,                                       │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-datetime-l1-1-0.dll'>,                                      │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-debug-l1-1-0.dll'>,                                         │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-errorhandling-l1-1-0.dll'>,                                 │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l1-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l1-2-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-file-l2-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-handle-l1-1-0.dll'>,                                        │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-heap-l1-1-0.dll'>,                                          │                                                                      │
│ │              │   <DirEntry 'api-ms-win-core-interlocked-l1-1-0.dll'>,                                   │                                                                      │
│ │              │   ... +110                                                                               │                                                                      │
│ │              ]                                                                                          │                                                                      │
│ │      entry = <DirEntry 'java.dll'>                                                                      │                                                                      │
│ │   fullname = 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin\\java.dll' │                                                                      │
│ │    onerror = <function rmtree.<locals>.onerror at 0x000002D702B0B0A0>                                   │                                                                      │
│ │       path = 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin'           │                                                                      │
│ │ scandir_it = <nt.ScandirIterator object at 0x000002D7020F6920>                                          │                                                                      │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
PermissionError: [WinError 5] Access is denied: 'C:\\Users\\user\\AppData\\Local\\BeeWare\\briefcase\\Cache\\tools\\java17\\bin\\java.dll'
```

</details>

### Additional context

_No response_

Hints from issue comments:
@freakboy3742: Fixed by #2199.

Solution patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3b302bb79..3f565057b 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -16,3 +16,11 @@ updates:
       interval: "weekly"
       day: "sunday"
       time: "20:00"
+
+  - package-ecosystem: "pip"
+    directory: "automation"
+    schedule:
+      # Check for updates on Sunday, 8PM UTC
+      interval: "weekly"
+      day: "sunday"
+      time: "20:00"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f856f77b8..b555a9e66 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -90,7 +90,7 @@ jobs:
         allow-prereleases: true
 
     - name: Get Packages
-      uses: actions/download-artifact@v4.1.9
+      uses: actions/download-artifact@v4.2.1
       with:
         name: ${{ needs.package.outputs.artifact-name }}
         path: dist
@@ -111,7 +111,7 @@ jobs:
 
     - name: Store Coverage Data
       if: always() && contains('success,failure', steps.test.outcome)
-      uses: actions/upload-artifact@v4.6.1
+      uses: actions/upload-artifact@v4.6.2
       with:
         name: coverage-data-${{ matrix.platform }}-${{ matrix.python-version }}
         path: ".coverage.*"
@@ -149,7 +149,7 @@ jobs:
         extra: dev
 
     - name: Retrieve Coverage Data
-      uses: actions/download-artifact@v4.1.9
+      uses: actions/download-artifact@v4.2.1
       with:
         pattern: coverage-data-*
         merge-multiple: true
@@ -167,7 +167,7 @@ jobs:
 
     - name: Upload Project Coverage HTML Report
       if: always() && steps.project-coverage.outcome == 'failure'
-      uses: actions/upload-artifact@v4.6.1
+      uses: actions/upload-artifact@v4.6.2
       with:
         name: html-coverage-report-project
         path: htmlcov
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bd40a773e..26863268e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -39,7 +39,7 @@ jobs:
           python-version: "3.x"
 
       - name: Get packages
-        uses: actions/download-artifact@v4.1.9
+        uses: actions/download-artifact@v4.2.1
         with:
           name: ${{ needs.ci.outputs.artifact-name }}
           path: dist
diff --git a/automation/pyproject.toml b/automation/pyproject.toml
index 97d8c848e..051d2e72c 100644
--- a/automation/pyproject.toml
+++ b/automation/pyproject.toml
@@ -1,8 +1,8 @@
 [build-system]
 requires = [
     # keep versions in sync with ../pyproject.toml
-    "setuptools==75.5.0",
-    "setuptools_scm==8.1.0",
+    "setuptools==77.0.3",
+    "setuptools_scm==8.2.0",
     "setuptools_dynamic_dependencies @ git+https://github.com/beeware/setuptools_dynamic_dependencies",
 ]
 build-backend = "setuptools.build_meta"
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
 name = "x-briefcase-automation"
 description = "A Briefcase plugin for CI automation."
 readme = "README.md"
-license.text = "New BSD"
+license = "BSD-3-Clause"
 classifiers = ["Private :: Do Not Upload"]
 dynamic = ["version", "dependencies"]
 
diff --git a/changes/1520.bugfix.rst b/changes/1520.bugfix.rst
new file mode 100644
index 000000000..078150124
--- /dev/null
+++ b/changes/1520.bugfix.rst
@@ -0,0 +1 @@
+Error handling during JDK upgrades has been improved.
diff --git a/changes/2103.feature.rst b/changes/2103.feature.rst
new file mode 100644
index 000000000..561ea064a
--- /dev/null
+++ b/changes/2103.feature.rst
@@ -0,0 +1 @@
+Cookiecutter filters for escaping XML content and attributes have been added.
diff --git a/changes/2128.feature.rst b/changes/2128.feature.rst
new file mode 100644
index 000000000..ad4930445
--- /dev/null
+++ b/changes/2128.feature.rst
@@ -0,0 +1 @@
+It's now possible to ask boolean question using the Console Interface.
diff --git a/changes/2204.removal.rst b/changes/2204.removal.rst
new file mode 100644
index 000000000..d8f145174
--- /dev/null
+++ b/changes/2204.removal.rst
@@ -0,0 +1 @@
+The ``app_packages`` folder now occurs *after* the ``app`` folder in the package resolution path on Windows, Linux, macOS and iOS. This will result in subtle changes in behavior if you have packages defined in your ``sources``  that have the same name as packages installed from your ``requires``.
diff --git a/changes/2205.misc.rst b/changes/2205.misc.rst
new file mode 100644
index 000000000..8392c05fd
--- /dev/null
+++ b/changes/2205.misc.rst
@@ -0,0 +1 @@
+Updated actions/download-artifact from 4.1.9 to 4.2.1.
diff --git a/changes/2206.misc.rst b/changes/2206.misc.rst
new file mode 100644
index 000000000..f67d204c3
--- /dev/null
+++ b/changes/2206.misc.rst
@@ -0,0 +1 @@
+Updated actions/upload-artifact from 4.6.1 to 4.6.2.
diff --git a/changes/2207.misc.rst b/changes/2207.misc.rst
new file mode 100644
index 000000000..c678c56a7
--- /dev/null
+++ b/changes/2207.misc.rst
@@ -0,0 +1 @@
+Updated setuptools from 76.0.0 to 77.0.3.
diff --git a/changes/2208.misc.rst b/changes/2208.misc.rst
new file mode 100644
index 000000000..b97ea5f19
--- /dev/null
+++ b/changes/2208.misc.rst
@@ -0,0 +1 @@
+Updated coverage[toml] from 7.7.0 to 7.7.1.
diff --git a/changes/2209.misc.rst b/changes/2209.misc.rst
new file mode 100644
index 000000000..1ae4f3354
--- /dev/null
+++ b/changes/2209.misc.rst
@@ -0,0 +1 @@
+Updated pre-commit from 4.1.0 to 4.2.0.
diff --git a/changes/2210.misc.rst b/changes/2210.misc.rst
new file mode 100644
index 000000000..20e6f02ba
--- /dev/null
+++ b/changes/2210.misc.rst
@@ -0,0 +1 @@
+Updated setuptools-scm from 8.1.0 to 8.2.0 in /automation.
diff --git a/changes/381.bugfix.rst b/changes/381.bugfix.rst
new file mode 100644
index 000000000..59042d487
--- /dev/null
+++ b/changes/381.bugfix.rst
@@ -0,0 +1 @@
+``.pth`` files created by packages installed as dependencies are now correctly processed during application startup on macOS, Windows, Linux and iOS.
diff --git a/pyproject.toml b/pyproject.toml
index 7d694e289..ebce90369 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [build-system]
 requires = [
     # keep versions in sync with automation/pyproject.toml
-    "setuptools==76.0.0",
+    "setuptools==77.0.3",
     "setuptools_scm==8.2.0",
 ]
 build-backend = "setuptools.build_meta"
@@ -12,7 +12,10 @@ name = "briefcase"
 description = "Tools to support converting a Python project into a standalone native application."
 readme = "README.rst"
 requires-python = ">= 3.9"
-license.text = "New BSD"
+license = "BSD-3-Clause"
+license-files = [
+    "LICENSE",
+]
 authors = [
     {name="Russell Keith-Magee", email="russell@keith-magee.com"},
 ]
@@ -45,7 +48,6 @@ keywords = [
 classifiers = [
     "Development Status :: 4 - Beta",
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: BSD License",
     "Operating System :: OS Independent",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",
@@ -95,9 +97,9 @@ dependencies = [
 # Extras used by developers *of* briefcase are pinned to specific versions to
 # ensure environment consistency.
 dev = [
-    "coverage[toml] == 7.7.0",
+    "coverage[toml] == 7.7.1",
     "coverage-conditional-plugin == 0.9.0",
-    "pre-commit == 4.1.0",
+    "pre-commit == 4.2.0",
     "pytest == 8.3.5",
     "pytest-xdist == 3.6.1",
     "setuptools_scm == 8.2.0",
diff --git a/src/briefcase/config.py b/src/briefcase/config.py
index 2c245870b..607f889de 100644
--- a/src/briefcase/config.py
+++ b/src/briefcase/config.py
@@ -201,6 +201,24 @@ def parsed_version(version):
     return SimpleNamespace(**groupdict)
 
 
+def parse_boolean(value: str) -> bool:
+    """Takes a string value and attempts to convert to a boolean value"""
+
+    truth_vals = {"true", "t", "yes", "y", "1", "on"}
+    false_vals = {"false", "f", "no", "n", "0", "off"}
+
+    normalised_val = value.strip().lower()
+
+    if normalised_val in truth_vals:
+        return True
+    elif normalised_val in false_vals:
+        return False
+    else:
+        raise ValueError(
+            f"Invalid boolean value: {value!r}. Expected one of {truth_vals | false_vals}"
+        )
+
+
 class BaseConfig:
     def __init__(self, **kwargs):
         for attr, value in kwargs.items():
diff --git a/src/briefcase/console.py b/src/briefcase/console.py
index 9384daf94..69df38b3d 100644
--- a/src/briefcase/console.py
+++ b/src/briefcase/console.py
@@ -30,6 +30,7 @@
 from rich.traceback import Trace, Traceback
 
 from briefcase import __version__
+from briefcase.config import parse_boolean
 from briefcase.exceptions import InputDisabled
 
 # Max width for printing to console; matches argparse's default width
@@ -732,7 +733,7 @@ def input_boolean(self, question: str, default: bool = False) -> bool:
         :returns: True if the user selected "y", or False if they selected "n".
         """
         if default is None:
-            yes_no = "y/n"
+            yes_no = "[y/n]"
             default_text = None
         elif default:
             yes_no = "[Y/n]"
@@ -960,3 +961,39 @@ def selection_question(
         )
 
         return ordered[int(index) - 1][0]
+
+    def boolean_question(
+        self,
+        description,
+        intro: str,
+        default: bool | None = None,
+        override_value: str | None = None,
+    ) -> bool:
+        """Ask the user a boolean question who's answer requires selecting yes/no
+
+        :param description: A short description of the question being asked. This text
+            is used in prompts and a header bar prefacing the question.
+        :param intro: An introductory paragraph explaining the question being asked.
+        :param default: The default option for empty user input.
+        :param override_value: A pre-selected answer for the question. This can be used
+            to shortcut asking the question, such as when a command line option provides
+            a value. If provided and valid, the header bar will be displayed, but the
+            intro paragraph and option list will not. Will take the provided string and attempt to parse into bool
+        :returns: The user's chosen answer or none if closed without input
+        """
+
+        self.divider(title=description)
+
+        if override_value is not None:
+            self.print()
+            self.print(f"Using override value {override_value!r}")
+            try:
+                return parse_boolean(override_value)
+            except ValueError as e:
+                raise ValueError(f"Invalid override value for {description}: {e}")
+
+        self.prompt()
+        self.prompt(self.textwrap(intro))
+        self.prompt()
+
+        return self.input_boolean(description, default=default)
diff --git a/src/briefcase/integrations/cookiecutter.py b/src/briefcase/integrations/cookiecutter.py
index d691b2892..b9ac45402 100644
--- a/src/briefcase/integrations/cookiecutter.py
+++ b/src/briefcase/integrations/cookiecutter.py
@@ -1,6 +1,7 @@
 """Jinja2 extensions."""
 
 import uuid
+from xml.sax.saxutils import escape, quoteattr
 
 from jinja2.ext import Extension
 
@@ -139,7 +140,17 @@ def bool_attr(obj):
             """Render value in XML format appropriate for an attribute."""
             return "true" if obj else "false"
 
+        def xml_escape(obj):
+            """Filter to escape characters <, >, &, " and '"""
+            return escape(obj)
+
+        def xml_attr(obj):
+            """ "Filter to quote an XML value appropriately."""
+            return quoteattr(obj)
+
         environment.filters["bool_attr"] = bool_attr
+        environment.filters["xml_escape"] = xml_escape
+        environment.filters["xml_attr"] = xml_attr
 
 
 class UUIDExtension(Extension):
diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py
index 7e0684a7b..e363ec7d9 100644
--- a/src/briefcase/integrations/java.py
+++ b/src/briefcase/integrations/java.py
@@ -310,8 +310,19 @@ def install(self):
 
     def uninstall(self):
         """Uninstall a JDK."""
-        with self.tools.console.wait_bar("Removing old JDK install..."):
-            if self.tools.host_os == "Darwin":
-                self.tools.shutil.rmtree(self.java_home.parent.parent)
-            else:
-                self.tools.shutil.rmtree(self.java_home)
+        try:
+            with self.tools.console.wait_bar("Removing old JDK install..."):
+                if self.tools.host_os == "Darwin":
+                    jdk_location = self.java_home.parent.parent
+                else:
+                    jdk_location = self.java_home
+                self.tools.shutil.rmtree(jdk_location)
+
+        except PermissionError as e:
+            raise BriefcaseCommandError(
+                f"""\
+Permission denied when trying to remove {jdk_location!r}
+
+Ensure no Java processes are running and try again.
+"""
+            ) from e


Test patch:
diff --git a/tests/config/test_parse_boolean.py b/tests/config/test_parse_boolean.py
new file mode 100644
index 000000000..a022bd7d7
--- /dev/null
+++ b/tests/config/test_parse_boolean.py
@@ -0,0 +1,33 @@
+import pytest
+
+from briefcase.config import parse_boolean
+
+
+@pytest.mark.parametrize(
+    "user_input_val",
+    ["true", "TRUE", "t", "T", "yes", "YES", "y", "Y", "1", "on", "ON"],
+)
+def test_true_values(user_input_val):
+    assert parse_boolean(user_input_val) is True
+
+
+@pytest.mark.parametrize(
+    "user_input_val",
+    ["false", "FALSE", "f", "F", "no", "NO", "n", "N", "0", "off", "OFF"],
+)
+def test_false_values(user_input_val):
+    assert parse_boolean(user_input_val) is False
+
+
+@pytest.mark.parametrize("user_input_val", ["maybe", "2", "", "help"])
+def test_invalid_values(user_input_val):
+    with pytest.raises(ValueError):
+        parse_boolean(user_input_val)
+
+
+@pytest.mark.parametrize(
+    "user_input_val, expected",
+    [("         yEs ", True), ("nO          ", False), ("    YEs", True)],
+)
+def test_whitespace(user_input_val, expected):
+    assert parse_boolean(user_input_val) is expected
diff --git a/tests/console/Console/test_boolean_question.py b/tests/console/Console/test_boolean_question.py
new file mode 100644
index 000000000..b55b889c5
--- /dev/null
+++ b/tests/console/Console/test_boolean_question.py
@@ -0,0 +1,108 @@
+import re
+
+import pytest
+
+from tests.utils import DummyConsole
+
+
+def test_boolean_question_yes():
+    """Test that boolean_question returns True when user selects Yes."""
+    console = DummyConsole("y")
+
+    result = console.boolean_question(
+        description="Confirm?",
+        intro="Are you sure?",
+        default=None,
+    )
+
+    assert console.prompts == ["Confirm? [y/n]? "]
+    assert result is True
+
+
+def test_boolean_question_no():
+    """Test that boolean_question returns False when user selects No."""
+    console = DummyConsole("n")
+
+    result = console.boolean_question(
+        description="Confirm?",
+        intro="Are you sure?",
+        default=None,
+    )
+
+    assert console.prompts == ["Confirm? [y/n]? "]
+    assert result is False
+
+
+@pytest.mark.parametrize(
+    "default, expected, prompt",
+    [
+        (True, True, "Confirm? [Y/n]? "),
+        (False, False, "Confirm? [y/N]? "),
+    ],
+)
+def test_boolean_question_default_used(default, expected, prompt):
+    """If no input is provided, the default value should be used."""
+    console = DummyConsole("")
+
+    result = console.boolean_question(
+        description="Confirm?",
+        intro="Are you sure?",
+        default=default,
+    )
+
+    assert console.prompts == [prompt]
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "override_value, expected_result",
+    [
+        ("Yes", True),
+        ("no", False),
+    ],
+)
+def test_boolean_question_override_used(capsys, override_value, expected_result):
+    """The override is used if provided and valid (parametrized for True/False cases)."""
+    console = DummyConsole()
+
+    result = console.boolean_question(
+        description="Confirm?",
+        intro="Are you sure?",
+        default=None,
+        override_value=override_value,
+    )
+
+    output = capsys.readouterr().out
+    assert f"Using override value {override_value!r}" in output
+    assert result == expected_result
+    assert console.prompts == []
+
+
+def test_boolean_question_override_invalid():
+    """If override_value is invalid, an error should be raised immediately."""
+    console = DummyConsole()
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape("Invalid boolean value: 'Yeah Nah'. Expected one of "),
+    ):
+        console.boolean_question(
+            description="Confirm?",
+            intro="Are you sure?",
+            default=True,
+            override_value="Yeah Nah",
+        )
+
+
+def test_boolean_question_empty_input_no_default_returns_false():
+    """Reprompts the user when no override or default given"""
+    console = DummyConsole("", "", "n")  # Simulates pressing Enter
+
+    result = console.boolean_question(
+        description="Confirm?",
+        intro="Are you sure?",
+        default=None,
+        override_value=None,
+    )
+
+    assert result is False
diff --git a/tests/integrations/cookiecutter/test_XMLExtension.py b/tests/integrations/cookiecutter/test_XMLExtension.py
index 54b256a44..7b3199b1f 100644
--- a/tests/integrations/cookiecutter/test_XMLExtension.py
+++ b/tests/integrations/cookiecutter/test_XMLExtension.py
@@ -24,3 +24,41 @@ def test_bool_attr(value, expected):
     env.filters = {}
     XMLExtension(env)
     assert env.filters["bool_attr"](value) == expected
+
+
+@pytest.mark.parametrize(
+    "value, expected",
+    [
+        # No special characters
+        ("Hello World", "Hello World"),
+        # Ampersands escaped
+        ("Hello & World", "Hello &amp; World"),
+        # Less than
+        ("Hello < World", "Hello &lt; World"),
+        # Greater than
+        ("Hello > World", "Hello &gt; World"),
+    ],
+)
+def test_xml_escape(value, expected):
+    env = MagicMock()
+    env.filters = {}
+    XMLExtension(env)
+    assert env.filters["xml_escape"](value) == expected
+
+
+@pytest.mark.parametrize(
+    "value, expected",
+    [
+        # A single quote wrapped in double quotes
+        ("Hello ' World", '"Hello \' World"'),
+        # A double quote wrapped in single quotes
+        ('Hello " World', "'Hello \" World'"),
+        # A double quote and a single quote in the same string value
+        ("Hello \" And ' World", '"Hello &quot; And \' World"'),
+    ],
+)
+def test_xml_attr(value, expected):
+    env = MagicMock()
+    env.filters = {}
+    XMLExtension(env)
+    assert env.filters["xml_attr"](value) == expected
diff --git a/tests/integrations/java/test_JDK__uninstall.py b/tests/integrations/java/test_JDK__uninstall.py
new file mode 100644
index 000000000..efd943535
--- /dev/null
+++ b/tests/integrations/java/test_JDK__uninstall.py
@@ -0,0 +1,83 @@
+import re
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from briefcase.exceptions import (
+    BriefcaseCommandError,
+)
+from briefcase.integrations.java import JDK
+
+
+@pytest.mark.parametrize(
+    "host_os, java_home",
+    [
+        ("Linux", Path("tools", "java17")),
+        ("Windows", Path("tools", "java17")),
+        ("Darwin", Path("tools", "java17", "Contents", "Home")),
+    ],
+)
+def test_java_running(mock_tools, host_os, java_home, tmp_path):
+    """If Java is running, uninstall will raise an error message."""
+    # Mock os
+    mock_tools.host_os = host_os
+
+    # Create a mock of a previously installed Java version.
+    (tmp_path / java_home / "bin").mkdir(parents=True)
+
+    # Create an JDK wrapper
+    jdk = JDK(mock_tools, java_home=tmp_path / java_home)
+
+    # Mock shutil.rmtree so that it gives a permission error
+    mock_tools.shutil.rmtree.side_effect = PermissionError(
+        "File is being used by other processes"
+    )
+
+    # Set correct path according to each OS
+    if host_os == "Darwin":
+        expected_loc = (tmp_path / java_home).parent.parent
+    else:
+        expected_loc = tmp_path / java_home
+
+    # Attempting to uninstall JDK which will fail due to Permission Error
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=re.escape(f"Permission denied when trying to remove {expected_loc!r}"),
+    ):
+        jdk.uninstall()
+
+
+@pytest.mark.parametrize(
+    "host_os, java_home",
+    [
+        ("Linux", Path("tools", "java17")),
+        ("Windows", Path("tools", "java17")),
+        ("Darwin", Path("tools", "java17", "Contents", "Home")),
+    ],
+)
+def test_java_not_running(mock_tools, host_os, java_home, tmp_path):
+    """If Java is not running, uninstalling should proceed smoothly."""
+    # Mock os
+    mock_tools.host_os = host_os
+
+    # Create a mock of a previously installed Java version.
+    (tmp_path / java_home / "bin").mkdir(parents=True)
+
+    # Create an JDK wrapper
+    jdk = JDK(mock_tools, java_home=tmp_path / java_home)
+
+    # Set correct path according to each OS
+    if host_os == "Darwin":
+        expected_loc = (tmp_path / java_home).parent.parent
+    else:
+        expected_loc = tmp_path / java_home
+
+    # Mock so that shutil behave normally
+    mock_tools.shutil.rmtree = MagicMock()
+
+    # Uninstall JDK
+    jdk.uninstall()
+
+    # Verify that shutil works normally and that correct location is passed
+    mock_tools.shutil.rmtree.assert_called_once_with(expected_loc)