Repository: conda/conda-build

Issue:
Long `conda info` error messages for common exceptions
### Checklist

- [X] I added a descriptive title
- [X] I searched open reports and couldn't find a duplicate

### What happened?

Originally posted by @h-vetinari in https://github.com/conda-forge/conda-forge-ci-setup-feedstock/issues/296

---

After the switch to conda-build (in v4 of the ci setup), something that happens very regularly on feedstocks -- a failing build script -- will now (occasionally?) cause long error reports from conda, which make it more confusing where the error is coming from. Here's an example. Everything but the first and last two lines should not be there.
```
  ERROR: Failed building wheel for mesas
ERROR: Could not build wheels for mesas, which is required to install pyproject.toml-based projects

# >>>>>>>>>>>>>>>>>>>>>> ERROR REPORT <<<<<<<<<<<<<<<<<<<<<<

    Traceback (most recent call last):
      File "C:\Miniforge\lib\site-packages\conda\exception_handler.py", line 17, in __call__
        return func(*args, **kwargs)
      File "C:\Miniforge\lib\site-packages\conda\cli\main.py", line 83, in main_subshell
        exit_code = do_call(args, parser)
      File "C:\Miniforge\lib\site-packages\conda\cli\conda_argparse.py", line 140, in do_call
        result = plugin_subcommand.action(getattr(args, "_args", args))
      File "C:\Miniforge\lib\site-packages\conda_build\plugin.py", line 10, in build
        execute(*args, **kwargs)
      File "C:\Miniforge\lib\site-packages\conda_build\cli\main_build.py", line 568, in execute
        outputs = api.build(
      File "C:\Miniforge\lib\site-packages\conda_build\api.py", line 253, in build
        return build_tree(
      File "C:\Miniforge\lib\site-packages\conda_build\build.py", line 3804, in build_tree
        packages_from_this = build(
      File "C:\Miniforge\lib\site-packages\conda_build\build.py", line 2629, in build
        windows.build(
      File "C:\Miniforge\lib\site-packages\conda_build\windows.py", line 344, in build
        check_call_env(
      File "C:\Miniforge\lib\site-packages\conda_build\utils.py", line 445, in check_call_env
        return _func_defaulting_env_to_os_environ("call", *popenargs, **kwargs)
      File "C:\Miniforge\lib\site-packages\conda_build\utils.py", line 421, in _func_defaulting_env_to_os_environ
        raise subprocess.CalledProcessError(proc.returncode, _args)
    subprocess.CalledProcessError: Command '['cmd.exe', '/d', '/c', 'conda_build.bat']' returned non-zero exit status 1.

`$ C:\Miniforge\Scripts\conda-script.py build recipe -m .ci_support\win_64_python3.9.____cpython.yaml --suppress-variables --extra-meta flow_run_id=azure_20231108.1.2 remote_url=*** sha=dadb1c68ff0ffc1af148ec1922f6943671dfea26`

  environment variables:
BUILD_DEFINITIONFOLDERPATH=\
BUILD_REPOSITORY_LOCALPATH=D:\a\1\s
                 CIO_TEST=<not set>
                    CONDA=C:\Miniconda
    CONDA_ALLOW_SOFTLINKS=false
           CONDA_BLD_PATH=D:\\bld\\
        CONDA_DEFAULT_ENV=base
                CONDA_EXE=C:\Miniforge\condabin\..\Scripts\conda.exe
CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED=1
             CONDA_PREFIX=C:\Miniforge
    CONDA_PROMPT_MODIFIER=(base)
         CONDA_PYTHON_EXE=C:\Miniforge\python.exe
               CONDA_ROOT=C:\Miniforge
              CONDA_SHLVL=1
             CONDA_SOLVER=libmamba
           CURL_CA_BUNDLE=<not set>
           ENTRYPOINTPATH=C:\Miniforge\Scripts\SetPageFileSize.ps1
                 HOMEPATH=\Users\VssAdministrator
               LD_PRELOAD=<not set>
                     PATH=C:\Miniforge;C:\Miniforge\Library\mingw-
                          w64\bin;C:\Miniforge\Library\usr\bin;C:\Miniforge\Library\bin;C:\Minif
                          orge\Scripts;C:\Miniforge\bin;C:\Miniforge\condabin;C:\Miniforge\Scrip
                          ts;C:\agents\3.230.0\externals\git\cmd;C:\agents\3.230.0\externals\git
                          \mingw64\bin;C:\PROGRA~1\MongoDB\bin;C:\aliyun-cli;C:\vcpkg;C:\Program
                          Files (x86)\NSIS;C:\tools\zstd;C:\Program Files\Mercurial;C:\h\windows
                          \stack\2.13.1\x64;C:\cabal\bin;C:\ghcup\bin;C:\mingw64\bin;C:\Program
                          Files\dotnet;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\Program
                          Files\R\R-4.3.2\bin\x64;C:\SeleniumWebDrivers\GeckoDriver;C:\Program
                          Files (x86)\GitHub CLI;C:\Program Files\Git\bin;C:\Program Files (x86)
                          \pipx_bin;C:\npm\prefix;C:\h\windows\go\1.20.11\x64\bin;C:\h\windows\P
                          ython\3.9.13\x64\Scripts;C:\h\windows\Python\3.9.13\x64;C:\h\windows\R
                          uby\3.0.6\x64\bin;C:\tools\kotlinc\bin;C:\h\windows\Java_Temurin-
                          Hotspot_jdk\8.0.392-8\x64\bin;C:\Program
                          Files\ImageMagick-7.1.1-Q16-HDRI;C:\Program Files\Microsoft SDKs\Azure
If submitted, this report will be used by core maintainers to improve
                          \CLI2\wbin;C:\ProgramData\kind;C:\Windows\system32;C:\Windows;C:\Windo
future releases of conda.
                          ws\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0;C:\Windows
                          \System32\OpenSSH;C:\Program Files\dotnet;C:\Program
                          Files\PowerShell\7;C:\Program Files\Microsoft\Web Platform
                          Installer;C:\Program Files\Microsoft SQL Server\Client
                          SDK\ODBC\170\Tools\Binn;C:\Program Files\Microsoft SQL
                          Server\150\Tools\Binn;C:\Program Files (x86)\Windows Kits\10\Windows
                          Performance Toolkit;C:\Program Files\Microsoft SQL
                          Server\130\DTS\Binn;C:\Program Files\Microsoft SQL
                          Server\140\DTS\Binn;C:\Program Files\Microsoft SQL
                          Server\150\DTS\Binn;C:\Program Files\Microsoft SQL Server\160\DTS\Binn
                          ;C:\ProgramData\chocolatey\lib\pulumi\tools\Pulumi\bin;C:\Program
                          Files\TortoiseSVN\bin;C:\ProgramData\chocolatey\lib\maven\apache-
                          maven-3.8.7\bin;C:\Program Files\Microsoft Service
                          Fabric\bin\Fabric\Fabric.Code;C:\Program Files\Microsoft SDKs\Service
                          Fabric\Tools\ServiceFabricLocalClusterManager;C:\Program
                          Files\nodejs;C:\Program Files\GitHub CLI;C:\SeleniumWebDrivers\ChromeD
                          river;C:\SeleniumWebDrivers\EdgeDriver;C:\Program
                          Files\Amazon\AWSCLIV2;C:\Program
                          Files\Amazon\SessionManagerPlugin\bin;C:\Program
                          Files\Amazon\AWSSAMCLI\bin;C:\Program Files\Microsoft SQL
                          Server\130\Tools\Binn;C:\Program Files\LLVM\bin;C:\Users\VssAdministra
                          tor\.dotnet\tools;C:\Users\VssAdministrator\.cargo\bin;C:\Users\VssAdm
                          inistrator\AppData\Local\Microsoft\WindowsApps
PSMODULEANALYSISCACHEPATH=C:\PSModuleAnalysisCachePath\ModuleAnalysisCache
             PSMODULEPATH=C:\Users\VssAdministrator\Documents\WindowsPowerShell\Modules;C:\\Modu
                          les\azurerm_2.1.0;C:\\Modules\azure_2.1.0;C:\Users\packer\Documents\Wi
                          ndowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\
                          Windows\system32\WindowsPowerShell\v1.0\Modules;C:\Program
                          Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\
         PYTHONUNBUFFERED=1
       REQUESTS_CA_BUNDLE=<not set>
        SELENIUM_JAR_PATH=C:\selenium\selenium-server.jar
            SSL_CERT_FILE=<not set>

     active environment : base
    active env location : C:\Miniforge
            shell level : 1
       user config file : C:\Users\VssAdministrator\.condarc
 populated config files : C:\Miniforge\.condarc
                          C:\Users\VssAdministrator\.condarc
          conda version : 23.10.0
    conda-build version : 3.27.0
         python version : 3.10.12.final.0
       virtual packages : __archspec=1=x86_64
                          __win=0=0
       base environment : C:\Miniforge  (writable)
      conda av data dir : C:\Miniforge\etc\conda
  conda av metadata url : None
           channel URLs : https://conda.anaconda.org/conda-forge/win-64
                          https://conda.anaconda.org/conda-forge/noarch
          package cache : C:\Miniforge\pkgs
                          C:\Users\VssAdministrator\.conda\pkgs
                          C:\Users\VssAdministrator\AppData\Local\conda\conda\pkgs
       envs directories : C:\Miniforge\envs
                          C:\Users\VssAdministrator\.conda\envs
                          C:\Users\VssAdministrator\AppData\Local\conda\conda\envs
               platform : win-64
             user-agent : conda/23.10.0 requests/2.31.0 CPython/3.10.12 Windows/10 Windows/10.0.20348 solver/libmamba conda-libmamba-solver/23.11.1 libmambapy/1.5.3
          administrator : True
             netrc file : None
           offline mode : False


An unexpected error has occurred. Conda has prepared the above report.
If you suspect this error is being caused by a malfunctioning plugin,
consider using the --no-plugins option to turn off plugins.

Example: conda --no-plugins install <package>

Alternatively, you can set the CONDA_NO_PLUGINS environment variable on
the command line to run the command without plugins enabled.

Example: CONDA_NO_PLUGINS=true conda install <package>


No report sent. To permanently opt-out, use

    $ conda config --set report_errors false

##[error]Cmd.exe exited with code '1'.
##[section]Finishing: Run Windows build
```

CC @jaimergp, who asked me to open this issue after it came up in discussion in last week's core call.

### Conda Info

```shell
See above
```


### Conda Config

```shell
N/A
```


### Conda list

```shell
N/A
```


### Additional Context

@mbargull shared more details in the [Element chat](https://matrix.to/#/!zjZSkaZgcvxBwdCKne:matrix.org/$qXDquFedtLyl7RhInilMYXJ9ELkJT4pDnXsRVQNuoUQ?via=matrix.org&via=mozilla.org):

> We applied https://github.com/conda-forge/conda-smithy/pull/1859 for conda-forge's builds.
> Essentially, it's conda-build non-clean exits via raising exceptions (e.g., CalledProcessError in case a build script fails) and then conda handling those exceptions with its internal exception handler if conda build is run (i.e., not conda-build directly but via subcommand plugin).
> Essentially we need 2 issues (in case there aren't any already):
>
> 1. issue for conda-build to more cleanly exit in error cases,
> 2. issue for conda to better handle non-internal exceptions from subcommand plugins.

> Oh, and if you need a test case for this, then a simple recipe with a failing build script should suffice, e.g.,
> ```bash
> ./meta.yaml cat << 'EOF'
> package:
>  name: pkg
>  version: 1.0
> build:
>  script: exit 1
> EOF
> conda build .
> ```



Discussion:
This is a reproducer test with current conda and conda-build 24.3.0:

```python
def test_conda_build_errors_without_conda_info_handlers(tmp_path, testing_workdir):
    ""
    recipe = tmp_path / "meta.yaml"
    recipe.write_text(
        dedent(
            """
            package:
                name: pkg
                version: '1.0'
            build:
                script: exit 1
            """
        )
    )
    process = subprocess.run(["conda", "build", tmp_path], capture_output=True, text=True, check=False)
    assert process.returncode > 0
    all_output = process.stdout + "\n" + process.stderr

    # These should NOT appear in the report
    assert ">>> ERROR REPORT <<<" not in all_output
    assert "An unexpected error has occurred." not in all_output
    assert "Conda has prepared the above report." not in all_output

    # These should appear
    assert "CalledProcessError" in all_output
    assert "returned non-zero exit status 1" in all_output
```

After looking into it, I see several ways forward:

- Make conda-build raise exceptions that are children of `conda.exceptions.CondaError`. This will involve a few changes all around the codebase.
- Make conda-build raise exceptions that are children of `conda_build.exceptions.CondaBuildError` and then have the conda plugin system recognize that as a known exception type.
- Do (2) and _also_ add a general mechanism in conda so plugins can define which base class their exceptions will be based on, so conda error reporting doesn't panic that loudly.

Something like:

```diff
  @conda.plugins.hookimpl
  def conda_subcommands():
      yield conda.plugins.CondaSubcommand(
          name="build",
          summary="Build conda packages from a conda recipe.",
          action=build,
+         base_exception_class=CondaBuildError,
      )
```

Then the plugin manager will be able to collect those and expose them to the [exception handler](https://github.com/jaimergp/conda/blob/bfc82e177904c8e18ebcb5deed25de69eb558369/conda/exception_handler.py#L49) so they can be checked before falling back to `handler_unexpected_exception`.

WDYT @conda/conda-core?

Resolving patch:
diff --git a/conda_build/build.py b/conda_build/build.py
--- a/conda_build/build.py
+++ b/conda_build/build.py
@@ -42,7 +42,11 @@
 from . import environ, noarch_python, source, tarcheck, utils
 from .config import Config
 from .create_test import create_all_test_files
-from .exceptions import CondaBuildException, DependencyNeedsBuildingError
+from .exceptions import (
+    BuildScriptException,
+    CondaBuildException,
+    DependencyNeedsBuildingError,
+)
 from .index import _delegated_update_index, get_build_index
 from .metadata import FIELDS, MetaData
 from .os_utils import external
@@ -1781,12 +1785,15 @@ def bundle_conda(output, metadata: MetaData, env, stats, **kw):
             _write_activation_text(dest_file, metadata)
 
         bundle_stats = {}
-        utils.check_call_env(
-            [*args, dest_file],
-            cwd=metadata.config.work_dir,
-            env=env_output,
-            stats=bundle_stats,
-        )
+        try:
+            utils.check_call_env(
+                [*args, dest_file],
+                cwd=metadata.config.work_dir,
+                env=env_output,
+                stats=bundle_stats,
+            )
+        except subprocess.CalledProcessError as exc:
+            raise BuildScriptException(str(exc), caused_by=exc) from exc
         log_stats(bundle_stats, f"bundling {metadata.name()}")
         if stats is not None:
             stats[stats_key(metadata, f"bundle_{metadata.name()}")] = bundle_stats
@@ -2459,9 +2466,12 @@ def build(
 
                     with codecs.getwriter("utf-8")(open(build_file, "wb")) as bf:
                         bf.write(script)
-                windows.build(
-                    m, build_file, stats=build_stats, provision_only=provision_only
-                )
+                try:
+                    windows.build(
+                        m, build_file, stats=build_stats, provision_only=provision_only
+                    )
+                except subprocess.CalledProcessError as exc:
+                    raise BuildScriptException(str(exc), caused_by=exc) from exc
             else:
                 build_file = join(m.path, "build.sh")
                 if isfile(build_file) and script:
@@ -2503,13 +2513,16 @@ def build(
                         del env["CONDA_BUILD"]
 
                         # this should raise if any problems occur while building
-                        utils.check_call_env(
-                            cmd,
-                            env=env,
-                            rewrite_stdout_env=rewrite_env,
-                            cwd=src_dir,
-                            stats=build_stats,
-                        )
+                        try:
+                            utils.check_call_env(
+                                cmd,
+                                env=env,
+                                rewrite_stdout_env=rewrite_env,
+                                cwd=src_dir,
+                                stats=build_stats,
+                            )
+                        except subprocess.CalledProcessError as exc:
+                            raise BuildScriptException(str(exc), caused_by=exc) from exc
                         utils.remove_pycache_from_scripts(m.config.host_prefix)
             if build_stats and not provision_only:
                 log_stats(build_stats, f"building {m.name()}")
diff --git a/conda_build/exceptions.py b/conda_build/exceptions.py
--- a/conda_build/exceptions.py
+++ b/conda_build/exceptions.py
@@ -2,12 +2,14 @@
 # SPDX-License-Identifier: BSD-3-Clause
 import textwrap
 
+from conda import CondaError
+
 SEPARATOR = "-" * 70
 
 indent = lambda s: textwrap.fill(textwrap.dedent(s))
 
 
-class CondaBuildException(Exception):
+class CondaBuildException(CondaError):
     pass
 
 
@@ -107,22 +109,26 @@ class BuildLockError(CondaBuildException):
     """Raised when we failed to acquire a lock."""
 
 
-class OverLinkingError(RuntimeError):
+class OverLinkingError(RuntimeError, CondaBuildException):
     def __init__(self, error, *args):
         self.error = error
         self.msg = f"overlinking check failed \n{error}"
         super().__init__(self.msg)
 
 
-class OverDependingError(RuntimeError):
+class OverDependingError(RuntimeError, CondaBuildException):
     def __init__(self, error, *args):
         self.error = error
         self.msg = f"overdepending check failed \n{error}"
         super().__init__(self.msg)
 
 
-class RunPathError(RuntimeError):
+class RunPathError(RuntimeError, CondaBuildException):
     def __init__(self, error, *args):
         self.error = error
         self.msg = f"runpaths check failed \n{error}"
         super().__init__(self.msg)
+
+
+class BuildScriptException(CondaBuildException):
+    pass


Test patch:
diff --git a/tests/test-recipes/metadata/_build_script_errors/output_build_script/meta.yaml b/tests/test-recipes/metadata/_build_script_errors/output_build_script/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_build_script_errors/output_build_script/meta.yaml
@@ -0,0 +1,10 @@
+package:
+  name: pkg
+  version: '1.0'
+source:
+  path: .
+outputs:
+  - name: pkg-output
+    build:
+      script:
+        - exit 1
diff --git a/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.bat b/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.bat
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.bat
@@ -0,0 +1 @@
+exit 1
\ No newline at end of file
diff --git a/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.sh b/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.sh
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_build_script_errors/output_script/exit_1.sh
@@ -0,0 +1 @@
+exit 1
\ No newline at end of file
diff --git a/tests/test-recipes/metadata/_build_script_errors/output_script/meta.yaml b/tests/test-recipes/metadata/_build_script_errors/output_script/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_build_script_errors/output_script/meta.yaml
@@ -0,0 +1,9 @@
+package:
+  name: pkg
+  version: '1.0'
+source:
+  path: .
+outputs:
+  - name: pkg-output
+    script: exit_1.sh   # [unix]
+    script: exit_1.bat  # [win]
diff --git a/tests/test-recipes/metadata/_build_script_errors/toplevel/meta.yaml b/tests/test-recipes/metadata/_build_script_errors/toplevel/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_build_script_errors/toplevel/meta.yaml
@@ -0,0 +1,7 @@
+package:
+  name: pkg
+  version: '1.0'
+source:
+  path: .
+build:
+  script: exit 1
diff --git a/tests/test_api_build.py b/tests/test_api_build.py
--- a/tests/test_api_build.py
+++ b/tests/test_api_build.py
@@ -36,6 +36,7 @@
 from conda_build import __version__, api, exceptions
 from conda_build.config import Config
 from conda_build.exceptions import (
+    BuildScriptException,
     CondaBuildException,
     DependencyNeedsBuildingError,
     OverDependingError,
@@ -383,7 +384,7 @@ def test_dirty_variable_available_in_build_scripts(testing_config):
     testing_config.dirty = True
     api.build(recipe, config=testing_config)
 
-    with pytest.raises(subprocess.CalledProcessError):
+    with pytest.raises(BuildScriptException):
         testing_config.dirty = False
         api.build(recipe, config=testing_config)
 
@@ -816,13 +817,13 @@ def test_disable_pip(testing_metadata):
     testing_metadata.meta["build"]["script"] = (
         'python -c "import pip; print(pip.__version__)"'
     )
-    with pytest.raises(subprocess.CalledProcessError):
+    with pytest.raises(BuildScriptException):
         api.build(testing_metadata)
 
     testing_metadata.meta["build"]["script"] = (
         'python -c "import setuptools; print(setuptools.__version__)"'
     )
-    with pytest.raises(subprocess.CalledProcessError):
+    with pytest.raises(BuildScriptException):
         api.build(testing_metadata)
 
 
@@ -1539,7 +1540,7 @@ def test_setup_py_data_in_env(testing_config):
     # should pass with any modern python (just not 3.5)
     api.build(recipe, config=testing_config)
     # make sure it fails with our special python logic
-    with pytest.raises(subprocess.CalledProcessError):
+    with pytest.raises(BuildScriptException):
         api.build(recipe, config=testing_config, python="3.5")
 
 
@@ -1945,7 +1946,7 @@ def test_add_pip_as_python_dependency_from_condarc_file(
     testing_metadata, testing_workdir, add_pip_as_python_dependency, monkeypatch
 ):
     """
-    Test whether settings from .condarc files are heeded.
+    Test whether settings from .condarc files are needed.
     ref: https://github.com/conda/conda-libmamba-solver/issues/393
     """
     # TODO: SubdirData._cache_ clearing might not be needed for future conda versions.
@@ -1961,10 +1962,44 @@ def test_add_pip_as_python_dependency_from_condarc_file(
     if add_pip_as_python_dependency:
         check_build_fails = nullcontext()
     else:
-        check_build_fails = pytest.raises(subprocess.CalledProcessError)
+        check_build_fails = pytest.raises(BuildScriptException)
 
     conda_rc = Path(testing_workdir, ".condarc")
     conda_rc.write_text(f"add_pip_as_python_dependency: {add_pip_as_python_dependency}")
     with env_var("CONDARC", conda_rc, reset_context):
         with check_build_fails:
             api.build(testing_metadata)
+
+
+@pytest.mark.parametrize(
+    "recipe", sorted(Path(metadata_dir, "_build_script_errors").glob("*"))
+)
+@pytest.mark.parametrize("debug", (False, True))
+def test_conda_build_script_errors_without_conda_info_handlers(tmp_path, recipe, debug):
+    env = os.environ.copy()
+    if debug:
+        env["CONDA_VERBOSITY"] = "3"
+    process = subprocess.run(
+        ["conda", "build", recipe],
+        env=env,
+        capture_output=True,
+        text=True,
+        check=False,
+        cwd=tmp_path,
+    )
+    assert process.returncode > 0
+    all_output = process.stdout + "\n" + process.stderr
+
+    # These should NOT appear in the output
+    assert ">>> ERROR REPORT <<<" not in all_output
+    assert "An unexpected error has occurred." not in all_output
+    assert "Conda has prepared the above report." not in all_output
+
+    # These should appear
+    assert "returned non-zero exit status 1" in all_output
+
+    # With verbose mode, we should actually see the traceback
+    if debug:
+        assert "Traceback" in all_output
+        assert "CalledProcessError" in all_output
+        assert "returned non-zero exit status 1" in all_output