Skip to content

Commit

Permalink
Add core:warnings_as_errors configuration option (#15149)
Browse files Browse the repository at this point in the history
* Add core:warnings_as_errors configuration option

* Extract the configuration of the ConanApp class to a new method

* Raise on error()

* Warnings as errors now raises on most .error() calls

* Revert some new ConanExceptions

* Revert error->warning

* Merge from release/2.0, add missing tests, change to exception

* Fix tests
  • Loading branch information
AbrilRBS authored Dec 18, 2023
1 parent 86557a9 commit 97b8b6d
Show file tree
Hide file tree
Showing 28 changed files with 338 additions and 236 deletions.
26 changes: 21 additions & 5 deletions conan/api/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ConanOutput:
# Singleton
_conan_output_level = LEVEL_STATUS
_silent_warn_tags = []
_warnings_as_errors = False

def __init__(self, scope=""):
self.stream = sys.stderr
Expand All @@ -63,6 +64,10 @@ def __init__(self, scope=""):
def define_silence_warnings(cls, warnings):
cls._silent_warn_tags = warnings or []

@classmethod
def set_warnings_as_errors(cls, value):
cls._warnings_as_errors = value

@classmethod
def define_log_level(cls, v):
"""
Expand Down Expand Up @@ -204,16 +209,27 @@ def success(self, msg):
return self

def warning(self, msg, warn_tag=None):
if self._conan_output_level <= LEVEL_WARNING:
if warn_tag is not None and warn_tag in self._silent_warn_tags:
def _skip_warning():
return (warn_tag is not None and warn_tag in self._silent_warn_tags
or warn_tag is None and "unknown" in self._silent_warn_tags)
if self._conan_output_level <= LEVEL_WARNING or (self._warnings_as_errors and self._conan_output_level <= LEVEL_ERROR):
if _skip_warning():
return self
warn_tag_msg = "" if warn_tag is None else f"{warn_tag}: "
self._write_message(f"WARN: {warn_tag_msg}{msg}", Color.YELLOW)
output = f"{warn_tag_msg}{msg}"

if self._warnings_as_errors:
self.error(output)
else:
self._write_message(f"WARN: {output}", Color.YELLOW)
return self

def error(self, msg):
def error(self, msg, error_type=None):
if self._conan_output_level <= LEVEL_ERROR:
self._write_message("ERROR: {}".format(msg), Color.RED)
if self._warnings_as_errors and error_type != "exception":
raise ConanException(msg)
else:
self._write_message("ERROR: {}".format(msg), Color.RED)
return self

def flush(self):
Expand Down
17 changes: 9 additions & 8 deletions conan/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def _add_commands(self):
try:
self._add_command(module_name, module_name.replace("cmd_", ""))
except Exception as e:
ConanOutput().error("Error loading custom command "
"'{}.py': {}".format(module_name, e))
ConanOutput().error(f"Error loading custom command '{module_name}.py': {e}",
error_type="exception")
# layers
for folder in os.listdir(custom_commands_path):
layer_folder = os.path.join(custom_commands_path, folder)
Expand All @@ -74,7 +74,8 @@ def _add_commands(self):
self._add_command(module_path, module_name.replace("cmd_", ""),
package=folder)
except Exception as e:
ConanOutput().error(f"Error loading custom command {module_path}: {e}")
ConanOutput().error(f"Error loading custom command {module_path}: {e}",
error_type="exception")

def _add_command(self, import_path, method_name, package=None):
try:
Expand Down Expand Up @@ -205,20 +206,20 @@ def exception_exit_error(exception):
if exception is None:
return SUCCESS
if isinstance(exception, ConanInvalidConfiguration):
output.error(exception)
output.error(exception, error_type="exception")
return ERROR_INVALID_CONFIGURATION
if isinstance(exception, ConanException):
output.error(exception)
output.error(exception, error_type="exception")
return ERROR_GENERAL
if isinstance(exception, SystemExit):
if exception.code != 0:
output.error("Exiting with code: %d" % exception.code)
output.error("Exiting with code: %d" % exception.code, error_type="exception")
return exception.code

assert isinstance(exception, Exception)
output.error(traceback.format_exc())
output.error(traceback.format_exc(), error_type="exception")
msg = exception_message_safe(exception)
output.error(msg)
output.error(msg, error_type="exception")
return ERROR_UNEXPECTED


Expand Down
5 changes: 3 additions & 2 deletions conan/internal/api/detect_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def default_msvc_runtime(compiler):
def default_cppstd(compiler, compiler_version):
""" returns the default cppstd for the compiler-version. This is not detected, just the default
"""

def _clang_cppstd_default(version):
if version >= "16":
return "gnu17"
Expand Down Expand Up @@ -269,8 +270,8 @@ def detect_compiler():
return gcc, gcc_version
if platform.system() == "SunOS" and command.lower() == "cc":
return _sun_cc_compiler(command)
if platform.system() == "Windows" and command.rstrip('"').endswith(("cl", "cl.exe")) \
and "clang" not in command:
if (platform.system() == "Windows" and command.rstrip('"').endswith(("cl", "cl.exe"))
and "clang" not in command):
return _msvc_cl_compiler(command)

# I am not able to find its version
Expand Down
2 changes: 1 addition & 1 deletion conan/internal/cache/db/cache_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CacheDatabase:

def __init__(self, filename):
version = sqlite3.sqlite_version
if Version(version) < "3.7.11": # Not an exception, in case some false positives
if Version(version) < "3.7.11":
ConanOutput().error(f"Your sqlite3 '{version} < 3.7.11' version is not supported")
self._recipes = RecipesDBTable(filename)
self._packages = PackagesDBTable(filename)
Expand Down
10 changes: 8 additions & 2 deletions conan/internal/conan_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ def __init__(self, requester, cmd_wrapper, global_conf, cache):

class ConanApp(object):
def __init__(self, cache_folder, global_conf):

self._configure(global_conf)
self.cache_folder = cache_folder
self.cache = ClientCache(self.cache_folder, global_conf)

home_paths = HomePaths(self.cache_folder)
self.hook_manager = HookManager(home_paths.hooks_path)

# Wraps an http_requester to inject proxies, certs, etc
ConanOutput.define_silence_warnings(global_conf.get("core:skip_warnings", check_type=list))
self.requester = ConanRequester(global_conf, cache_folder)
# To handle remote connections
rest_client_factory = RestApiClientFactory(self.requester, global_conf)
Expand All @@ -61,3 +61,9 @@ def __init__(self, cache_folder, global_conf):
cmd_wrap = CmdWrapper(home_paths.wrapper_path)
conanfile_helpers = ConanFileHelpers(self.requester, cmd_wrap, global_conf, self.cache)
self.loader = ConanFileLoader(self.pyreq_loader, conanfile_helpers)

@staticmethod
def _configure(global_conf):
ConanOutput.set_warnings_as_errors(global_conf.get("core:warnings_as_errors",
default=False, check_type=bool))
ConanOutput.define_silence_warnings(global_conf.get("core:skip_warnings", check_type=list))
2 changes: 1 addition & 1 deletion conan/internal/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _deploy_single(dep, conanfile, output_folder, folder_name):
except Exception as e:
if "WinError 1314" in str(e):
ConanOutput().error("full_deploy: Symlinks in Windows require admin privileges "
"or 'Developer mode = ON'")
"or 'Developer mode = ON'", error_type="exception")
raise ConanException(f"full_deploy: The copy of '{dep}' files failed: {e}.\nYou can "
f"use 'tools.deployer:symlinks' conf to disable symlinks")
dep.set_deploy_folder(new_folder)
Expand Down
12 changes: 6 additions & 6 deletions conan/internal/integrity_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def _recipe_corrupted(self, ref: RecipeReference):
if not k.startswith("export_source")}

if read_manifest != expected_manifest:
output.error(f"{ref}: Manifest mismatch")
output.error(f"Folder: {layout.export()}")
output.error(f"{ref}: Manifest mismatch", error_type="exception")
output.error(f"Folder: {layout.export()}", error_type="exception")
diff = read_manifest.difference(expected_manifest)
for fname, (h1, h2) in diff.items():
output.error(f" '{fname}' (manifest: {h1}, file: {h2})")
output.error(f" '{fname}' (manifest: {h1}, file: {h2})", error_type="exception")
return True
output.info(f"{ref}: Integrity checked: ok")

Expand All @@ -55,10 +55,10 @@ def _package_corrupted(self, ref: PkgReference):
read_manifest, expected_manifest = layout.package_manifests()

if read_manifest != expected_manifest:
output.error(f"{ref}: Manifest mismatch")
output.error(f"Folder: {layout.package()}")
output.error(f"{ref}: Manifest mismatch", error_type="exception")
output.error(f"Folder: {layout.package()}", error_type="exception")
diff = read_manifest.difference(expected_manifest)
for fname, (h1, h2) in diff.items():
output.error(f" '{fname}' (manifest: {h1}, file: {h2})")
output.error(f" '{fname}' (manifest: {h1}, file: {h2})", error_type="exception")
return True
output.info(f"{ref}: Integrity checked: ok")
4 changes: 2 additions & 2 deletions conan/tools/files/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def print_progress(_, __):
try:
z.extract(file_, full_path)
except Exception as e:
output.error("Error extract %s\n%s" % (file_.filename, str(e)))
output.error(f"Error extract {file_.filename}\n{str(e)}", error_type="exception")
else: # duplicated for, to avoid a platform check for each zipped file
for file_ in zip_info:
extracted_size += file_.file_size
Expand All @@ -352,7 +352,7 @@ def print_progress(_, __):
perm = file_.external_attr >> 16 & 0xFFF
os.chmod(os.path.join(full_path, file_.filename), perm)
except Exception as e:
output.error("Error extract %s\n%s" % (file_.filename, str(e)))
output.error(f"Error extract {file_.filename}\n{str(e)}", error_type="exception")
output.writeln("")


Expand Down
2 changes: 1 addition & 1 deletion conans/client/cmd/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def cmd_export(app, global_conf, conanfile_path, name, version, user, channel, g
clean_dirty(source_folder)
except BaseException as e:
scoped_output.error("Unable to delete source folder. Will be marked as corrupted "
"for deletion")
"for deletion", error_type="exception")
scoped_output.warning(str(e))
set_dirty(source_folder)

Expand Down
2 changes: 1 addition & 1 deletion conans/client/downloaders/file_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def download(self, url, file_path, retry=2, retry_wait=0, verify_ssl=True, auth=
if counter == retry:
raise
else:
self._output.error(exc)
self._output.warning(exc, warn_tag="network")
self._output.info(f"Waiting {retry_wait} seconds to retry...")
time.sleep(retry_wait)

Expand Down
2 changes: 1 addition & 1 deletion conans/client/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def write_generators(conanfile, app):
continue
except Exception as e:
# When a generator fails, it is very useful to have the whole stacktrace
conanfile.output.error(traceback.format_exc())
conanfile.output.error(traceback.format_exc(), error_type="exception")
raise ConanException(f"Error in generator '{generator_name}': {str(e)}") from e
finally:
# restore the generators attribute, so it can raise
Expand Down
4 changes: 4 additions & 0 deletions conans/client/graph/build_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ def should_build_missing(self, conanfile):
for pattern in self.build_missing_patterns:
if ref_matches(conanfile.ref, pattern, is_consumer=False):
return True

def report_matches(self):
for pattern in self._unused_patterns:
ConanOutput().error(f"No package matching '{pattern}' pattern found.", error_type="context")
2 changes: 1 addition & 1 deletion conans/client/graph/install_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def _raise_missing(self, missing):
missing_prefs_str = list(sorted([str(pref) for pref in missing_prefs]))
out = ConanOutput()
for pref_str in missing_prefs_str:
out.error("Missing binary: %s" % pref_str)
out.error(f"Missing binary: {pref_str}", error_type="exception")
out.writeln("")

# Report details just the first one
Expand Down
2 changes: 1 addition & 1 deletion conans/client/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _build(self, conanfile, pref):
conanfile.output.success("Package '%s' built" % pref.package_id)
conanfile.output.info("Build folder %s" % conanfile.build_folder)
except Exception as exc:
conanfile.output.error("\nPackage '%s' build failed" % pref.package_id)
conanfile.output.error(f"\nPackage '{pref.package_id}' build failed", error_type="exception")
conanfile.output.warning("Build folder %s" % conanfile.build_folder)
if isinstance(exc, ConanException):
raise exc
Expand Down
3 changes: 2 additions & 1 deletion conans/client/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def _migrate_pkg_db_lru(cache_folder, old_version):
f"INTEGER DEFAULT '{lru}' NOT NULL;")
except Exception:
ConanOutput().error(f"Could not complete the 2.0.14 DB migration."
" Please manually remove your .conan2 cache and reinstall packages")
" Please manually remove your .conan2 cache and reinstall packages",
error_type="exception")
raise
else: # generate the back-migration script
undo_lru = textwrap.dedent("""\
Expand Down
12 changes: 6 additions & 6 deletions conans/client/remote_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def get_recipe(self, ref, remote, metadata=None):
f"no conanmanifest.txt")
self._signer.verify(ref, download_export, files=zipped_files)
except BaseException: # So KeyboardInterrupt also cleans things
ConanOutput(scope=str(ref)).error(f"Error downloading from remote '{remote.name}'")
ConanOutput(scope=str(ref)).error(f"Error downloading from remote '{remote.name}'", error_type="exception")
self._cache.remove_recipe_layout(layout)
raise
export_folder = layout.export()
Expand Down Expand Up @@ -91,7 +91,7 @@ def get_recipe_metadata(self, ref, remote, metadata):
self._call_remote(remote, "get_recipe", ref, download_export, metadata,
only_metadata=True)
except BaseException: # So KeyboardInterrupt also cleans things
output.error(f"Error downloading metadata from remote '{remote.name}'")
output.error(f"Error downloading metadata from remote '{remote.name}'", error_type="exception")
raise

def get_recipe_sources(self, ref, layout, remote):
Expand Down Expand Up @@ -134,8 +134,8 @@ def get_package_metadata(self, pref, remote, metadata):
self._call_remote(remote, "get_package", pref, download_pkg_folder,
metadata, only_metadata=True)
except BaseException as e: # So KeyboardInterrupt also cleans things
output.error("Exception while getting package metadata: %s" % str(pref.package_id))
output.error("Exception: %s %s" % (type(e), str(e)))
output.error(f"Exception while getting package metadata: {str(pref.package_id)}", error_type="exception")
output.error(f"Exception: {type(e)} {str(e)}", error_type="exception")
raise

def _get_package(self, layout, pref, remote, scoped_output, metadata):
Expand Down Expand Up @@ -166,8 +166,8 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata):
raise PackageNotFoundException(pref)
except BaseException as e: # So KeyboardInterrupt also cleans things
self._cache.remove_package_layout(layout)
scoped_output.error("Exception while getting package: %s" % str(pref.package_id))
scoped_output.error("Exception: %s %s" % (type(e), str(e)))
scoped_output.error(f"Exception while getting package: {str(pref.package_id)}", error_type="exception")
scoped_output.error(f"Exception: {type(e)} {str(e)}", error_type="exception")
raise

def search_recipes(self, remote, pattern):
Expand Down
6 changes: 3 additions & 3 deletions conans/client/rest/auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ def _retry_with_new_token(self, user, remote, method_name, *args, **kwargs):
except AuthenticationException:
out = ConanOutput()
if user is None:
out.error('Wrong user or password')
out.error('Wrong user or password', error_type="exception")
else:
out.error('Wrong password for user "%s"' % user)
out.error(f'Wrong password for user "{user}"', error_type="exception")
out.info('You can change username with "conan remote login <remote> <username>"')
else:
return self.call_rest_api_method(remote, method_name, *args, **kwargs)
Expand All @@ -98,7 +98,7 @@ def _clear_user_tokens_in_db(self, user, remote):
self._localdb.store(user, token=None, refresh_token=None, remote_url=remote.url)
except Exception as e:
out = ConanOutput()
out.error('Your credentials could not be stored in local cache\n')
out.error('Your credentials could not be stored in local cache\n', error_type="exception")
out.debug(str(e) + '\n')

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion conans/client/rest/file_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def upload(self, url, abs_path, auth=None, dedup=False, retry=None, retry_wait=N
raise
else:
if self._output:
self._output.error(exc)
self._output.warning(exc, warn_tag="network")
self._output.info("Waiting %d seconds to retry..." % retry_wait)
time.sleep(retry_wait)

Expand Down
2 changes: 1 addition & 1 deletion conans/client/rest/rest_client_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _upload_files(self, files, urls):
except (AuthenticationException, ForbiddenException):
raise
except Exception as exc:
output.error("\nError uploading file: %s, '%s'" % (filename, exc))
output.error(f"\nError uploading file: {filename}, '{exc}'", error_type="exception")
failed.append(filename)

if failed:
Expand Down
2 changes: 1 addition & 1 deletion conans/client/userio.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,5 @@ def request_boolean(self, msg, default_option=None):
elif s.lower() in ['no', 'n']:
ret = False
else:
self._out.error("%s is not a valid answer" % s)
self._out.error(f"{s} is not a valid answer")
return ret
5 changes: 3 additions & 2 deletions conans/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def migrate(self):
self._apply_back_migrations()
self._update_version_file()
except Exception as e:
ConanOutput().error(str(e))
ConanOutput().error(str(e), error_type="exception")
raise ConanMigrationError(e)

def _update_version_file(self):
Expand Down Expand Up @@ -81,5 +81,6 @@ def _apply_back_migrations(self):
migrate_method(self.conf_path)
except Exception as e:
ConanOutput().error(f"There was an error running downgrade migration: {e}. "
f"Recommended to remove the cache and start from scratch")
f"Recommended to remove the cache and start from scratch",
error_type="exception")
os.remove(migration)
1 change: 1 addition & 0 deletions conans/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BUILT_IN_CONFS = {
"core:required_conan_version": "Raise if current version does not match the defined range.",
"core:non_interactive": "Disable interactive user input, raises error if input necessary",
"core:warnings_as_errors": "Treat warnings as errors",
"core:skip_warnings": "Do not show warnings in this list",
"core:default_profile": "Defines the default host profile ('default' by default)",
"core:default_build_profile": "Defines the default build profile ('default' by default)",
Expand Down
Loading

0 comments on commit 97b8b6d

Please sign in to comment.