diff --git a/.gitignore b/.gitignore index 4813690..79fc120 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build/ tmp/ upload.* .idea/ -build_tmp.py \ No newline at end of file +build_tmp.py +__pycache__ diff --git a/linter/__init__.py b/linter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linter/check_import_conanfile.py b/linter/check_import_conanfile.py new file mode 100644 index 0000000..bd5a903 --- /dev/null +++ b/linter/check_import_conanfile.py @@ -0,0 +1,28 @@ + +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from astroid import nodes, Const, AssignName + + +class ImportConanFile(BaseChecker): + """ + Import ConanFile from new 'conan' module + """ + + __implements__ = IAstroidChecker + + name = "conan-import-conanfile" + msgs = { + "E9006": ( + "Import ConanFile from new module: `from conan import ConanFile`. Old import is deprecated in Conan v2.", + "conan-import-conanfile", + "Import ConanFile from new module: `from conan import ConanFile`. Old import is deprecated in Conan v2.", + ), + } + + def visit_importfrom(self, node: nodes.ImportFrom) -> None: + basename = node.modname + if basename == 'conans': + names = [name for name, _ in node.names] + if 'ConanFile' in names: + self.add_message("conan-import-conanfile", node=node) diff --git a/linter/check_import_errors.py b/linter/check_import_errors.py new file mode 100644 index 0000000..b6a0ac1 --- /dev/null +++ b/linter/check_import_errors.py @@ -0,0 +1,77 @@ + +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from astroid import nodes, Const, AssignName + + +class ImportErrors(BaseChecker): + """ + Import errors from new 'conan' module + """ + + __implements__ = IAstroidChecker + + name = "conan-import-errors" + msgs = { + "E9008": ( + "Import errors from new module: `from conan import errors`. Old import is deprecated in Conan v2.", + "conan-import-errors", + "Import errors from new module: `from conan import errors`. Old import is deprecated in Conan v2.", + ), + } + + def visit_importfrom(self, node: nodes.ImportFrom) -> None: + basename = node.modname + if basename == 'conans': + names = [name for name, _ in node.names] + if 'errors' in names: + self.add_message("conan-import-errors", node=node) + + +class ImportErrorsConanException(BaseChecker): + """ + Import errors from new 'conan' module + """ + + __implements__ = IAstroidChecker + + name = "conan-import-error-conanexception" + msgs = { + "E9009": ( + "Import ConanException from new module: `from conan.errors import ConanException`. Old import is deprecated in Conan v2.", + "conan-import-error-conanexception", + "Import ConanException from new module: `from conan.errors import ConanException`. Old import is deprecated in Conan v2.", + ), + } + + def visit_importfrom(self, node: nodes.ImportFrom) -> None: + basename = node.modname + if basename == 'conans.errors': + names = [name for name, _ in node.names] + if 'ConanException' in names: + self.add_message("conan-import-error-conanexception", node=node) + + +class ImportErrorsConanInvalidConfiguration(BaseChecker): + """ + Import errors from new 'conan' module + """ + + __implements__ = IAstroidChecker + + name = "conan-import-error-conaninvalidconfiguration" + msgs = { + "E9010": ( + "Import ConanInvalidConfiguration from new module: `from conan.errors import ConanInvalidConfiguration`. Old import is deprecated in Conan v2.", + "conan-import-error-conaninvalidconfiguration", + "Import ConanInvalidConfiguration from new module: `from conan.errors import ConanInvalidConfiguration`. Old import is deprecated in Conan v2.", + ), + } + + def visit_importfrom(self, node: nodes.ImportFrom) -> None: + basename = node.modname + if basename == 'conans.errors': + names = [name for name, _ in node.names] + if 'ConanInvalidConfiguration' in names: + self.add_message("conan-import-error-conaninvalidconfiguration", node=node) + diff --git a/linter/check_import_tools.py b/linter/check_import_tools.py new file mode 100644 index 0000000..1621c7b --- /dev/null +++ b/linter/check_import_tools.py @@ -0,0 +1,30 @@ +import re +from email.mime import base +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from astroid import nodes, Const, AssignName + + +class ImportTools(BaseChecker): + """ + Import tools following pattern 'from conan.tools.xxxx import yyyyy' + """ + + __implements__ = IAstroidChecker + + name = "conan-import-tools" + msgs = { + "E9011": ( + "Import tools following pattern 'from conan.tools.xxxx import yyyyy' (https://docs.conan.io/en/latest/reference/conanfile/tools.html).", + "conan-import-tools", + "Import tools following pattern 'from conan.tools.xxxx import yyyyy' (https://docs.conan.io/en/latest/reference/conanfile/tools.html).", + ), + } + + def visit_importfrom(self, node: nodes.ImportFrom) -> None: + basename = node.modname + names = [name for name, _ in node.names] + if basename == 'conan' and 'tools' in names: + self.add_message("conan-import-tools", node=node) + elif re.match(r'conan\.tools\.[^.]+\..+', basename): + self.add_message("conan-import-tools", node=node) diff --git a/linter/check_no_test_package_name.py b/linter/check_no_test_package_name.py new file mode 100644 index 0000000..6a92747 --- /dev/null +++ b/linter/check_no_test_package_name.py @@ -0,0 +1,30 @@ +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from astroid import nodes, Const, AssignName + + +class NoPackageName(BaseChecker): + """ + Conanfile used for testing a package should NOT provide a name + """ + + __implements__ = IAstroidChecker + + name = "conan-test-package-name" + msgs = { + "E9007": ( + "No 'name' attribute in test_package conanfile", + "conan-test-no-name", + "No 'name' attribute in test_package conanfile." + ) + } + + def visit_classdef(self, node: nodes) -> None: + if node.basenames == ['ConanFile']: + for attr in node.body: + children = list(attr.get_children()) + if len(children) == 2 and \ + isinstance(children[0], AssignName) and \ + children[0].name == "name" and \ + isinstance(children[1], Const): + self.add_message("conan-test-no-name", node=attr, line=attr.lineno) diff --git a/linter/check_package_name.py b/linter/check_package_name.py new file mode 100644 index 0000000..d2901a6 --- /dev/null +++ b/linter/check_package_name.py @@ -0,0 +1,39 @@ +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from astroid import nodes, Const, AssignName + + +class PackageName(BaseChecker): + """ + All packages must have a lower-case name + """ + + __implements__ = IAstroidChecker + + name = "conan-package-name" + msgs = { + "E9004": ( + "Reference name should be all lowercase", + "conan-bad-name", + "Use only lower-case on the package name: `name = 'foobar'`." + ), + "E9005": ( + "Missing name attribute", + "conan-missing-name", + "The member attribute `name` must be declared: `name = 'foobar'`." + ) + } + + def visit_classdef(self, node: nodes) -> None: + if node.basenames == ['ConanFile']: + for attr in node.body: + children = list(attr.get_children()) + if len(children) == 2 and \ + isinstance(children[0], AssignName) and \ + children[0].name == "name" and \ + isinstance(children[1], Const): + value = children[1].as_string() + if value.lower() != value: + self.add_message("conan-bad-name", node=attr, line=attr.lineno) + return + self.add_message("conan-missing-name", node=node) diff --git a/linter/conanv2_test_transition.py b/linter/conanv2_test_transition.py new file mode 100644 index 0000000..105891a --- /dev/null +++ b/linter/conanv2_test_transition.py @@ -0,0 +1,20 @@ +""" + +Pylint plugin/rules for test_package folder in Conan Center Index + +""" + +from pylint.lint import PyLinter +from linter.check_import_conanfile import ImportConanFile +from linter.check_no_test_package_name import NoPackageName +from linter.check_import_errors import ImportErrorsConanException, ImportErrorsConanInvalidConfiguration, ImportErrors +from linter.check_import_tools import ImportTools + + +def register(linter: PyLinter) -> None: + linter.register_checker(NoPackageName(linter)) + linter.register_checker(ImportConanFile(linter)) + linter.register_checker(ImportErrors(linter)) + linter.register_checker(ImportErrorsConanException(linter)) + linter.register_checker(ImportErrorsConanInvalidConfiguration(linter)) + linter.register_checker(ImportTools(linter)) diff --git a/linter/conanv2_transition.py b/linter/conanv2_transition.py new file mode 100644 index 0000000..8c79054 --- /dev/null +++ b/linter/conanv2_transition.py @@ -0,0 +1,20 @@ +""" + +Pylint plugin/rules for conanfiles in Conan Center Index + +""" + +from pylint.lint import PyLinter +from linter.check_package_name import PackageName +from linter.check_import_conanfile import ImportConanFile +from linter.check_import_errors import ImportErrorsConanException, ImportErrorsConanInvalidConfiguration, ImportErrors +from linter.check_import_tools import ImportTools + + +def register(linter: PyLinter) -> None: + linter.register_checker(PackageName(linter)) + linter.register_checker(ImportConanFile(linter)) + linter.register_checker(ImportErrors(linter)) + linter.register_checker(ImportErrorsConanException(linter)) + linter.register_checker(ImportErrorsConanInvalidConfiguration(linter)) + linter.register_checker(ImportTools(linter)) diff --git a/linter/pylintrc_recipe b/linter/pylintrc_recipe new file mode 100644 index 0000000..a6a0c3e --- /dev/null +++ b/linter/pylintrc_recipe @@ -0,0 +1,30 @@ +[MASTER] +load-plugins=linter.conanv2_transition, + linter.transform_conanfile, + linter.transform_imports + +py-version=3.6 +recursive=no +suggestion-mode=yes +unsafe-load-any-extension=no + +[MESSAGES CONTROL] +disable=fixme, + line-too-long, + missing-module-docstring, + missing-function-docstring, + missing-class-docstring, + invalid-name, + wrong-import-order, # TODO: Remove + import-outside-toplevel # TODO: Remove + +enable=conan-bad-name, + conan-missing-name, + conan-import-conanfile + +[REPORTS] +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error) / statement) * 10)) +output-format=text +reports=no +score=no + diff --git a/linter/pylintrc_testpackage b/linter/pylintrc_testpackage new file mode 100644 index 0000000..70d2a6a --- /dev/null +++ b/linter/pylintrc_testpackage @@ -0,0 +1,27 @@ +[MASTER] +load-plugins=linter.conanv2_test_transition, + linter.transform_conanfile, + linter.transform_imports +py-version=3.6 +recursive=no +suggestion-mode=yes +unsafe-load-any-extension=no + +[MESSAGES CONTROL] +disable=fixme, + line-too-long, + missing-module-docstring, + missing-function-docstring, + missing-class-docstring, + invalid-name, + wrong-import-order, # TODO: Remove + import-outside-toplevel # TODO: Remove + +enable=conan-test-no-name, + conan-import-conanfile + +[REPORTS] +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error) / statement) * 10)) +output-format=text +reports=no +score=no diff --git a/linter/recipe_linter.json b/linter/recipe_linter.json new file mode 100644 index 0000000..0b9ff45 --- /dev/null +++ b/linter/recipe_linter.json @@ -0,0 +1,43 @@ +{ + "problemMatcher": [ + { + "owner": "recipe_linter_fatals", + "severity": "error", + "pattern": [ + { + "regexp": "(\\S+):(\\d+): \\[(F\\d+\\(\\S+\\)),\\s(.+?)?\\](.+)", + "file": 1, + "line": 2, + "message": 5, + "code": 3 + } + ] + }, + { + "owner": "recipe_linter_errors", + "severity": "error", + "pattern": [ + { + "regexp": "(\\S+):(\\d+): \\[(E\\d+\\(\\S+\\)),\\s(.+?)?\\](.+)", + "file": 1, + "line": 2, + "message": 5, + "code": 3 + } + ] + }, + { + "owner": "recipe_linter_warnings", + "severity": "warning", + "pattern": [ + { + "regexp": "(\\S+):(\\d+): \\[(W\\d+\\(\\S+\\)),\\s(.+?)?\\](.+)", + "file": 1, + "line": 2, + "message": 5, + "code": 3 + } + ] + } + ] +} diff --git a/linter/transform_conanfile.py b/linter/transform_conanfile.py new file mode 100644 index 0000000..8e33d36 --- /dev/null +++ b/linter/transform_conanfile.py @@ -0,0 +1,74 @@ + +# Class ConanFile doesn't declare all the valid members and functions, +# some are injected by Conan dynamically to the class. + +import textwrap +import astroid +from astroid.builder import AstroidBuilder +from astroid.manager import AstroidManager + + +def _settings_transform(): + module = AstroidBuilder(AstroidManager()).string_build( + textwrap.dedent(""" + class Settings(object): + os = None + arch = None + compiler = None + build_type = None + """) + ) + return module['Settings'] + +def _user_info_build_transform(): + module = AstroidBuilder(AstroidManager()).string_build( + textwrap.dedent(""" + class UserInfoBuild(defaultdict): + pass + """) + ) + return module['UserInfoBuild'] + + +def register(_): + pass + +def transform_conanfile(node): + """Transform definition of ConanFile class so dynamic fields are visible to pylint""" + + str_class = astroid.builtin_lookup("str") + dict_class = astroid.builtin_lookup("dict") + info_class = astroid.MANAGER.ast_from_module_name("conans.model.info").lookup( + "ConanInfo") + build_requires_class = astroid.MANAGER.ast_from_module_name( + "conans.client.graph.graph_manager").lookup("_RecipeBuildRequires") + file_copier_class = astroid.MANAGER.ast_from_module_name( + "conans.client.file_copier").lookup("FileCopier") + file_importer_class = astroid.MANAGER.ast_from_module_name( + "conans.client.importer").lookup("_FileImporter") + python_requires_class = astroid.MANAGER.ast_from_module_name( + "conans.client.graph.python_requires").lookup("PyRequires") + + dynamic_fields = { + "conan_data": str_class, + "build_requires": build_requires_class, + "tool_requires": build_requires_class, + "info_build": info_class, + "user_info_build": [_user_info_build_transform()], + "info": info_class, + "copy": file_copier_class, + "copy_deps": file_importer_class, + "python_requires": [str_class, python_requires_class], + "recipe_folder": str_class, + "settings_build": [_settings_transform()], + "settings_target": [_settings_transform()], + "conf": dict_class, + } + + for f, t in dynamic_fields.items(): + node.locals[f] = [i for i in t] + + +astroid.MANAGER.register_transform( + astroid.ClassDef, transform_conanfile, + lambda node: node.qname() == "conans.model.conan_file.ConanFile") diff --git a/linter/transform_imports.py b/linter/transform_imports.py new file mode 100644 index 0000000..78b72e4 --- /dev/null +++ b/linter/transform_imports.py @@ -0,0 +1,46 @@ + +import astroid +from pylint.lint import PyLinter + +""" +Here we are transforming the imports to mimic future Conan v2 release. With +these changes, built-in checks in Pylint will raise with different errors, so +we are modifying the messages to point users in the right direction. +""" + + +def register(linter: PyLinter): + msge1101 = linter.msgs_store._messages_definitions["E1101"] + msge1101.msg += ". Please, check https://github.com/conan-io/conan-center-index/blob/master/docs/v2_linter.md" + linter.msgs_store.register_message(msge1101) + + msge0611 = linter.msgs_store._messages_definitions["E0611"] + msge0611.msg += ". Please, check https://github.com/conan-io/conan-center-index/blob/master/docs/v2_linter.md" + linter.msgs_store.register_message(msge0611) + +def transform_tools(module): + """ Transform import module """ + if 'get' in module.locals: + del module.locals['get'] + if 'cross_building' in module.locals: + del module.locals['cross_building'] + if 'rmdir' in module.locals: + del module.locals['rmdir'] + if 'Version' in module.locals: + del module.locals['Version'] + +def transform_errors(module): + pass + #if 'ConanInvalidConfiguration' in module.locals: + # del module.locals['ConanInvalidConfiguration'] + #if 'ConanException' in module.locals: + # del module.locals['ConanException'] + + +astroid.MANAGER.register_transform( + astroid.Module, transform_tools, + lambda node: node.qname() == "conans.tools") + +astroid.MANAGER.register_transform( + astroid.Module, transform_errors, + lambda node: node.qname() == "conans.errors") diff --git a/linter/yamllint_rules.yml b/linter/yamllint_rules.yml new file mode 100644 index 0000000..7c0ce10 --- /dev/null +++ b/linter/yamllint_rules.yml @@ -0,0 +1,26 @@ +extends: default +rules: + document-start: + level: error + present: false + document-end: + level: error + present: false + empty-values: + forbid-in-block-mappings: true + forbid-in-flow-mappings: true + line-length: disable + indentation: + level: error + new-line-at-end-of-file: + level: error + trailing-spaces: + level: error + comments: + level: error + comments-indentation: + level: error + new-lines: + type: unix + key-duplicates: + level: error diff --git a/socketw/all/conanfile.py b/socketw/all/conanfile.py index fd1eb90..c138a15 100644 --- a/socketw/all/conanfile.py +++ b/socketw/all/conanfile.py @@ -1,5 +1,6 @@ -from conans import ConanFile, CMake, tools - +from conan import ConanFile +from conan.tools.files import get, collect_libs +from conan.tools.cmake import CMakeToolchain, CMake, CMakeDeps, cmake_layout class SocketwConan(ConanFile): name = "socketw" @@ -7,14 +8,22 @@ class SocketwConan(ConanFile): url = "https://github.com/RigsOfRods/socketw/issues" description = "SocketW is a library which provides cross-platform socket abstraction" settings = "os", "compiler", "build_type", "arch" - generators = "cmake" + + def layout(self): + cmake_layout(self) def requirements(self): for req in self.conan_data["requirements"]: self.requires(req) def source(self): - tools.get(**self.conan_data["sources"][self.version], strip_root=True) + get(self, **self.conan_data["sources"][self.version], strip_root=True) + + def generate(self): + tc = CMakeToolchain(self) + tc.generate() + deps = CMakeDeps(self) + deps.generate() def build(self): cmake = CMake(self) @@ -26,4 +35,4 @@ class SocketwConan(ConanFile): cmake.install() def package_info(self): - self.cpp_info.libs = tools.collect_libs(self) + self.cpp_info.libs = collect_libs(self)