diff --git a/conda_build/build.py b/conda_build/build.py index 6dd2b49256..3c5512c7d5 100644 --- 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 index 9744ca14b4..8aa10149d9 100644 --- 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 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 index 0000000000..406ba464c0 --- /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 index 0000000000..6dedc57766 --- /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 index 0000000000..6dedc57766 --- /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 index 0000000000..43c2f9d054 --- /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 index 0000000000..df710d103b --- /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 index a663f18e73..8f431bfae2 100644 --- 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