Repository: beeware/briefcase

Issue:
Error message is misleading if XCode is installed by Cmdline tools are active
Originally reported as a [PR on beeware](https://github.com/beeware/beeware/pull/49)

I was following the beeware tutorial and I got the following error

```
(beeware-venv) $ briefcase create iOS
xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

  Xcode is not installed.

  You should be shown a dialog prompting you to install Xcode and the
  command line tools. Select "Get Xcode" to install Xcode from the app store.
  You can install Xcode from the macOS App Store.
  Re-run Briefcase once that installation is complete.
```

It took me a while to figure out how to solve this because I already had Xcode installed.

The solution was:
```
(beeware-venv) $ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
```

The problem is that the XCode *is* installed, but the command line tools are currently the active install. 

The error message needs to differentiate between:
* XCode command line tools are installed, but XCode isn't installed
* XCode command line tools *and* XCode are installed, but command line tools are active.

and present an appropriate error message in both cases.


Discussion:

Patch:
diff --git a/changes/397.bugfix.rst b/changes/397.bugfix.rst
new file mode 100644
index 00000000..ae513cae
--- /dev/null
+++ b/changes/397.bugfix.rst
@@ -0,0 +1,1 @@
+iOS builds will now warn if the Xcode command line tools are the active.
diff --git a/src/briefcase/integrations/xcode.py b/src/briefcase/integrations/xcode.py
index 11feebed..7c123dc9 100644
--- a/src/briefcase/integrations/xcode.py
+++ b/src/briefcase/integrations/xcode.py
@@ -148,6 +148,7 @@ Re-run Briefcase once that installation is complete.
     try:
         output = command.subprocess.check_output(
             ['xcodebuild', '-version'],
+            stderr=subprocess.STDOUT,
             universal_newlines=True
         )
 
@@ -198,16 +199,28 @@ Re-run Briefcase once that installation is complete.
 *************************************************************************
 """)
 
-    except subprocess.CalledProcessError:
-        raise BriefcaseCommandError("""
-Xcode is not installed.
+    except subprocess.CalledProcessError as e:
+        if " is a command line tools instance" in e.output:
+            raise BriefcaseCommandError("""
+Xcode is installed, but the active developer directory is a
+command line tools instance. To make XCode the active developer
+directory, run:
 
-You should be shown a dialog prompting you to install Xcode and the
-command line tools. Select "Get Xcode" to install Xcode from the app store.
+    $ sudo xcode-select -switch /Applications/Xcode.app
 
-You can install Xcode from the macOS App Store.
+and then re-run Briefcase.
+""")
+        else:
+            raise BriefcaseCommandError("""
+The Xcode install appears to exist, but Briefcase was unable to
+determine the current Xcode version. Running:
 
-Re-run Briefcase once that installation is complete.
+    $ xcodebuild -version
+
+should return the current Xcode version, but it raised an error.
+
+You may need to re-install Xcode. Re-run Briefcase once that
+installation is complete.
 """)
 
 


Test patch:
diff --git a/tests/integrations/xcode/test_ensure_xcode_is_installed.py b/tests/integrations/xcode/test_ensure_xcode_is_installed.py
index ba379cf2..b4d0272c 100644
--- a/tests/integrations/xcode/test_ensure_xcode_is_installed.py
+++ b/tests/integrations/xcode/test_ensure_xcode_is_installed.py
@@ -34,20 +34,49 @@ def test_not_installed(tmp_path):
     command.subprocess.check_output.assert_not_called()
 
 
-def test_exists_but_not_installed(xcode):
+def test_exists_but_command_line_tools_selected(xcode):
+    "If the Xcode folder exists, but cmd-line tools are selected, raise an error."
+    command = mock.MagicMock()
+    command.subprocess.check_output.side_effect = subprocess.CalledProcessError(
+        cmd=['xcodebuild', '-version'],
+        returncode=1
+    )
+    command.subprocess.check_output.side_effect.output = (
+        "xcode-select: error: tool 'xcodebuild' requires Xcode, but "
+        "active developer directory '/Library/Developer/CommandLineTools' "
+        "is a command line tools instance\n"
+    )
+
+    with pytest.raises(BriefcaseCommandError, match=r"xcode-select -switch"):
+        ensure_xcode_is_installed(command, xcode_location=xcode)
+
+    # xcode-select was invoked
+    command.subprocess.check_output.assert_called_once_with(
+        ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
+        universal_newlines=True,
+    )
+
+
+def test_exists_but_corrupted(xcode):
     "If the Xcode folder exists, but xcodebuild breaks, raise an error."
     command = mock.MagicMock()
     command.subprocess.check_output.side_effect = subprocess.CalledProcessError(
         cmd=['xcodebuild', '-version'],
         returncode=1
     )
+    command.subprocess.check_output.side_effect.output = "Badness occurred."
 
-    with pytest.raises(BriefcaseCommandError):
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=r"should return the current Xcode version"
+    ):
         ensure_xcode_is_installed(command, xcode_location=xcode)
 
     # xcode-select was invoked
     command.subprocess.check_output.assert_called_once_with(
         ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
         universal_newlines=True,
     )
 
@@ -63,6 +92,7 @@ def test_installed_no_minimum_version(xcode):
     # xcode-select was invoked
     command.subprocess.check_output.assert_called_once_with(
         ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
         universal_newlines=True,
     )
 
@@ -122,6 +152,7 @@ def test_installed_with_minimum_version_success(min_version, version, capsys, xc
     # xcode-select was invoked
     command.subprocess.check_output.assert_called_once_with(
         ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
         universal_newlines=True,
     )
 
@@ -160,6 +191,7 @@ def test_installed_with_minimum_version_failure(min_version, version, xcode):
     # xcode-select was invoked
     command.subprocess.check_output.assert_called_once_with(
         ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
         universal_newlines=True,
     )
 
@@ -179,6 +211,7 @@ def test_unexpected_version_output(capsys, xcode):
     # xcode-select was invoked
     command.subprocess.check_output.assert_called_once_with(
         ['xcodebuild', '-version'],
+        stderr=subprocess.STDOUT,
         universal_newlines=True,
     )