Skip to content

Commit 6b1a6fc

Browse files
committed
Support conan in packagedcode
Signed-off-by: Keshav Priyadarshi <[email protected]>
1 parent f70bbb7 commit 6b1a6fc

File tree

11 files changed

+2828
-0
lines changed

11 files changed

+2828
-0
lines changed

src/packagedcode/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from packagedcode import debian_copyright
2222
from packagedcode import distro
2323
from packagedcode import conda
24+
from packagedcode import conan
2425
from packagedcode import cocoapods
2526
from packagedcode import cran
2627
from packagedcode import freebsd
@@ -77,6 +78,8 @@
7778
conda.CondaYamlHandler,
7879
conda.CondaMetaYamlHandler,
7980

81+
conan.ConanFileHandler,
82+
8083
cran.CranDescriptionFileHandler,
8184

8285
debian_copyright.DebianCopyrightFileInPackageHandler,

src/packagedcode/conan.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright (c) nexB Inc. and others. All rights reserved.
2+
# ScanCode is a trademark of nexB Inc.
3+
# SPDX-License-Identifier: Apache-2.0
4+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
5+
# See https://github.com/nexB/scancode-toolkit for support or download.
6+
# See https://aboutcode.org for more information about nexB OSS projects.
7+
#
8+
import ast
9+
import io
10+
import logging
11+
import os
12+
13+
from packageurl import PackageURL
14+
15+
from packagedcode import models
16+
17+
"""
18+
Handle conanfile recipes for conan packages
19+
https://docs.conan.io/2/reference/conanfile.html
20+
"""
21+
22+
23+
SCANCODE_DEBUG_PACKAGE = os.environ.get("SCANCODE_DEBUG_PACKAGE", False)
24+
25+
TRACE = SCANCODE_DEBUG_PACKAGE
26+
27+
28+
def logger_debug(*args):
29+
pass
30+
31+
32+
logger = logging.getLogger(__name__)
33+
34+
if TRACE:
35+
import sys
36+
37+
logging.basicConfig(stream=sys.stdout)
38+
logger.setLevel(logging.DEBUG)
39+
40+
def logger_debug(*args):
41+
return logger.debug(" ".join(isinstance(a, str) and a or repr(a) for a in args))
42+
43+
44+
45+
class ConanFileParser(ast.NodeVisitor):
46+
def __init__(self):
47+
self.name = None
48+
self.version = None
49+
self.description = None
50+
self.author = None
51+
self.homepage_url = None
52+
self.vcs_url = None
53+
self.license = None
54+
self.keywords = []
55+
self.requires = []
56+
57+
def to_dict(self):
58+
return {
59+
'name': self.name,
60+
'version': self.version,
61+
'description': self.description,
62+
'author':self.author,
63+
'homepage_url':self.homepage_url,
64+
'vcs_url':self.vcs_url,
65+
'license':self.license,
66+
'keywords':self.keywords,
67+
'requires':self.requires,
68+
}
69+
70+
71+
def visit_Assign(self, node):
72+
if not node.targets or not isinstance(node.targets[0], ast.Name):
73+
return
74+
if not node.value or not (isinstance(node.value, ast.Constant) or isinstance(node.value, ast.Tuple)):
75+
return
76+
variable_name = node.targets[0].id
77+
values = node.value
78+
if variable_name == "name":
79+
self.name = values.value
80+
elif variable_name == "version":
81+
self.version = values.value
82+
elif variable_name == "description":
83+
self.description = values.value
84+
elif variable_name == "author":
85+
self.author = values.value
86+
elif variable_name == "homepage":
87+
self.homepage_url = values.value
88+
elif variable_name == "url":
89+
self.vcs_url = values.value
90+
elif variable_name == "license":
91+
self.license = values.value
92+
elif variable_name == "topics":
93+
self.keywords.extend(
94+
[el.value for el in values.elts if isinstance(el, ast.Constant)]
95+
)
96+
elif variable_name == "requires":
97+
if isinstance(values, ast.Tuple):
98+
self.requires.extend(
99+
[el.value for el in values.elts if isinstance(el, ast.Constant)]
100+
)
101+
elif isinstance(values, ast.Constant):
102+
self.requires.append(values.value)
103+
104+
def visit_Call(self, node):
105+
if not isinstance(node.func, ast.Attribute) or not isinstance(
106+
node.func.value, ast.Name
107+
):
108+
return
109+
if node.func.value.id == "self" and node.func.attr == "requires":
110+
if node.args and isinstance(node.args[0], ast.Constant):
111+
self.requires.append(node.args[0].value)
112+
113+
114+
class ConanFileHandler(models.DatafileHandler):
115+
datasource_id = "conan_conanfile_py"
116+
path_patterns = ("*/conanfile.py",)
117+
default_package_type = "conan"
118+
default_primary_language = "C++"
119+
description = "conan recipe"
120+
documentation_url = "https://docs.conan.io/2.0/reference/conanfile.html"
121+
122+
@classmethod
123+
def parse(cls, location):
124+
with io.open(location, encoding="utf-8") as loc:
125+
conan_recipe = loc.read()
126+
127+
try:
128+
tree = ast.parse(conan_recipe)
129+
recipe_class_def = next(
130+
(
131+
node
132+
for node in tree.body
133+
if isinstance(node, ast.ClassDef)
134+
and node.bases
135+
and isinstance(node.bases[0], ast.Name)
136+
and node.bases[0].id == "ConanFile"
137+
),
138+
None,
139+
)
140+
141+
parser = ConanFileParser()
142+
parser.visit(recipe_class_def)
143+
except SyntaxError as e:
144+
if TRACE:
145+
logger_debug(f"Syntax error in conan recipe: {e}")
146+
return
147+
148+
if TRACE:
149+
logger_debug(f"ConanFileHandler: parse: package: {parser.to_dict()}")
150+
151+
dependencies = get_dependencies(parser.requires)
152+
153+
yield models.PackageData(
154+
datasource_id=cls.datasource_id,
155+
type=cls.default_package_type,
156+
primary_language=cls.default_primary_language,
157+
namespace=None,
158+
name=parser.name,
159+
version=parser.version,
160+
description=parser.description,
161+
homepage_url=parser.homepage_url,
162+
vcs_url=parser.vcs_url,
163+
keywords=parser.keywords,
164+
declared_license_expression=parser.license,
165+
dependencies=dependencies,
166+
)
167+
168+
def is_constraint_resolved(constraint):
169+
range_characters = {'>', '<', '[', ']', '>=','<='}
170+
return not any(char in range_characters for char in constraint)
171+
172+
def get_dependencies(requires):
173+
dependent_packages=[]
174+
for req in requires:
175+
name, constraint = req.split('/', 1)
176+
is_resolved = is_constraint_resolved(constraint)
177+
purl = PackageURL(type='pypi', name=name)
178+
dependent_packages.append(
179+
models.DependentPackage(
180+
purl=purl.to_string(),
181+
scope='install',
182+
is_runtime=True,
183+
is_optional=False,
184+
is_resolved=is_resolved,
185+
extracted_requirement=constraint
186+
)
187+
)
188+
return dependent_packages

0 commit comments

Comments
 (0)