Skip to content

Commit a07a5fb

Browse files
authored
Merge pull request #2822 from nexB/detect-build-gradle
Detect dependencies from build.gradle files Reference: #793 Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
2 parents b8640f7 + dbef133 commit a07a5fb

7 files changed

Lines changed: 588 additions & 2 deletions

File tree

src/packagedcode/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from packagedcode import about
1313
from packagedcode import bower
1414
from packagedcode import build
15+
from packagedcode import build_gradle
1516
from packagedcode import cargo
1617
from packagedcode import chef
1718
from packagedcode import debian
@@ -107,7 +108,8 @@
107108
msi.MsiInstallerPackage,
108109
windows.MicrosoftUpdateManifest,
109110
pubspec.PubspecYaml,
110-
pubspec.PubspecLock
111+
pubspec.PubspecLock,
112+
build_gradle.BuildGradle,
111113
]
112114

113115
PACKAGE_MANIFESTS_BY_TYPE = {

src/packagedcode/build.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import attr
1616

17-
from commoncode import filetype
1817
from commoncode import fileutils
1918
from packagedcode import models
2019
from packagedcode.utils import combine_expressions

src/packagedcode/build_gradle.py

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# ScanCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/scancode-toolkit for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
12+
from packageurl import PackageURL
13+
from pygmars import Token
14+
from pygmars.parse import Parser
15+
from pygments import lex
16+
from pygments.lexers import GroovyLexer
17+
import attr
18+
19+
from packagedcode import models
20+
from packagedcode.build import BaseBuildManifestPackage
21+
22+
23+
TRACE = False
24+
25+
logger = logging.getLogger(__name__)
26+
27+
if TRACE:
28+
import sys
29+
logging.basicConfig(stream=sys.stdout)
30+
logger.setLevel(logging.DEBUG)
31+
32+
33+
grammar = """
34+
LIT-STRING: {<LITERAL-STRING-SINGLE|LITERAL-STRING-DOUBLE>}
35+
36+
PACKAGE-IDENTIFIER: {<OPERATOR> <TEXT>? <NAME-LABEL> <TEXT>? <LIT-STRING>}
37+
DEPENDENCY-1: {<PACKAGE-IDENTIFIER>{3} <OPERATOR>}
38+
39+
DEPENDENCY-2: {<NAME> <TEXT> <LIT-STRING> <TEXT>}
40+
41+
DEPENDENCY-3: {<NAME> <TEXT>? <OPERATOR> <LIT-STRING> <OPERATOR>}
42+
43+
DEPENDENCY-4: {<NAME> <TEXT> <NAME-LABEL> <TEXT> <LIT-STRING> <PACKAGE-IDENTIFIER> <PACKAGE-IDENTIFIER> <OPERATOR>? <TEXT>}
44+
45+
DEPENDENCY-5: {<NAME> <TEXT> <NAME> <OPERATOR> <NAME-ATTRIBUTE>}
46+
47+
NESTED-DEPENDENCY-1: {<NAME> <OPERATOR> <DEPENDENCY-1>+ }
48+
"""
49+
50+
51+
def get_tokens(contents):
52+
for i, (token, value) in enumerate(lex(contents, GroovyLexer())):
53+
yield i, token, value
54+
55+
56+
def get_pygmar_tokens(contents):
57+
tokens = Token.from_pygments_tokens(get_tokens(contents))
58+
for token in tokens:
59+
if token.label == 'NAME' and token.value == 'dependencies':
60+
token.label = 'DEPENDENCIES-START'
61+
yield token
62+
63+
64+
def get_parse_tree(build_gradle_location):
65+
# Open build.gradle and create a Pygmars parse tree from its contents
66+
with open(build_gradle_location) as f:
67+
contents = f.read()
68+
parser = Parser(grammar, trace=0)
69+
return parser.parse(list(get_pygmar_tokens(contents)))
70+
71+
72+
def is_literal_string(string):
73+
return string == 'LITERAL-STRING-SINGLE' or string == 'LITERAL-STRING-DOUBLE'
74+
75+
76+
def remove_quotes(string):
77+
"""
78+
Remove starting and ending quotes from `string`.
79+
80+
If `string` has no starting or ending quotes, return `string`.
81+
"""
82+
quoted = lambda x: (x.startswith('"') and x.endswith('"')) or (x.startswith("'") and x.endswith("'"))
83+
if quoted:
84+
return string[1:-1]
85+
else:
86+
return string
87+
88+
89+
def get_dependencies_from_parse_tree(parse_tree):
90+
dependencies = []
91+
in_dependency_block = False
92+
brackets_counter = 0
93+
first_bracket_seen = False
94+
in_nested_dependency = False
95+
nested_dependency_parenthesis_counter = 0
96+
first_parenthesis_seen = False
97+
for tree_node in parse_tree:
98+
if tree_node.label == 'DEPENDENCIES-START':
99+
in_dependency_block = True
100+
continue
101+
102+
if in_dependency_block:
103+
if tree_node.label == 'OPERATOR':
104+
if tree_node.value == '{':
105+
if not first_bracket_seen:
106+
first_bracket_seen = True
107+
brackets_counter += 1
108+
elif tree_node.value == '}':
109+
brackets_counter -= 1
110+
111+
if brackets_counter == 0 and first_bracket_seen:
112+
break
113+
114+
# TODO: Find way to simplify logic with DEPENDENCY-1
115+
if tree_node.label == 'NESTED-DEPENDENCY-1':
116+
dependency = {}
117+
in_nested_dependency = True
118+
scope = None
119+
last_key = None
120+
for child_node in tree_node.leaves():
121+
if child_node.label == 'NAME':
122+
scope = child_node.value
123+
124+
if child_node.label == 'OPERATOR' and child_node.value == '(':
125+
if not first_parenthesis_seen:
126+
first_parenthesis_seen = True
127+
nested_dependency_parenthesis_counter += 1
128+
129+
if child_node.label == 'NAME-LABEL':
130+
value = child_node.value
131+
if value == 'group:':
132+
last_key = 'namespace'
133+
if value == 'name:':
134+
last_key = 'name'
135+
if value == 'version:':
136+
last_key = 'version'
137+
138+
if is_literal_string(child_node.label):
139+
dependency[last_key] = remove_quotes(child_node.value)
140+
if scope:
141+
dependency['scope'] = scope
142+
dependencies.append(dependency)
143+
144+
if in_nested_dependency:
145+
if tree_node.label == 'OPERATOR' and tree_node.value == ')':
146+
nested_dependency_parenthesis_counter -= 1
147+
148+
if nested_dependency_parenthesis_counter == 0 and first_parenthesis_seen:
149+
in_nested_dependency = False
150+
scope = None
151+
152+
if tree_node.label == 'DEPENDENCY-1':
153+
name_label_to_dep_field_name = {
154+
'group:': 'namespace',
155+
'name:': 'name',
156+
'version:': 'version'
157+
}
158+
dependency = {}
159+
last_key = None
160+
for child_node in tree_node.leaves():
161+
value = child_node.value
162+
if child_node.label == 'NAME-LABEL':
163+
last_key = name_label_to_dep_field_name.get(value, '')
164+
if is_literal_string(child_node.label):
165+
if last_key:
166+
dependency[last_key] = remove_quotes(value)
167+
if in_nested_dependency and scope:
168+
dependency['scope'] = scope
169+
dependencies.append(dependency)
170+
171+
if tree_node.label == 'DEPENDENCY-2':
172+
dependency = {}
173+
for child_node in tree_node.leaves():
174+
if child_node.label == 'NAME':
175+
dependency['scope'] = child_node.value
176+
if is_literal_string(child_node.label):
177+
value = child_node.value
178+
value = remove_quotes(value)
179+
180+
namespace = ''
181+
name = ''
182+
version = ''
183+
split_value = value.split(':')
184+
split_value_length = len(split_value)
185+
if split_value_length == 4:
186+
# We are assuming `value` is in the form of "namespace:name:version:module"
187+
# We are currently not reporting down to the module level
188+
namespace, name, version, _ = split_value
189+
if split_value_length == 3:
190+
# We are assuming `value` is in the form of "namespace:name:version"
191+
namespace, name, version = split_value
192+
if split_value_length == 2:
193+
# We are assuming `value` is in the form of "namespace:name"
194+
namespace, name = split_value
195+
196+
dependency['namespace'] = namespace
197+
dependency['name'] = name
198+
dependency['version'] = version
199+
dependencies.append(dependency)
200+
201+
if tree_node.label == 'DEPENDENCY-3':
202+
dependency = {}
203+
for child_node in tree_node.leaves():
204+
if child_node.label == 'NAME':
205+
dependency['scope'] = child_node.value
206+
if is_literal_string(child_node.label):
207+
value = child_node.value
208+
value = remove_quotes(value)
209+
# We are assuming `value` is in the form of "namespace:name:version"
210+
split_dependency_string = value.split(':')
211+
if len(split_dependency_string) != 3:
212+
break
213+
namespace, name, version = split_dependency_string
214+
dependency['namespace'] = namespace
215+
dependency['name'] = name
216+
dependency['version'] = version
217+
dependencies.append(dependency)
218+
219+
# TODO: See if you can refactor logic with DEPENDENCY-1
220+
if tree_node.label == 'DEPENDENCY-4':
221+
dependency = {}
222+
last_key = None
223+
for child_node in tree_node.leaves():
224+
if child_node.label == 'NAME':
225+
dependency['scope'] = child_node.value
226+
if child_node.label == 'NAME-LABEL':
227+
value = child_node.value
228+
if value == 'group:':
229+
last_key = 'namespace'
230+
if value == 'name:':
231+
last_key = 'name'
232+
if value == 'version:':
233+
last_key = 'version'
234+
if is_literal_string(child_node.label):
235+
dependency[last_key] = remove_quotes(child_node.value)
236+
dependencies.append(dependency)
237+
238+
if tree_node.label == 'DEPENDENCY-5':
239+
dependency = {}
240+
for child_node in tree_node.leaves():
241+
if child_node.label == 'NAME':
242+
dependency['scope'] = child_node.value
243+
if child_node.label == 'NAME-ATTRIBUTE':
244+
dependency['name'] = child_node.value
245+
dependencies.append(dependency)
246+
return dependencies
247+
248+
249+
def get_dependencies(build_gradle_location):
250+
parse_tree = get_parse_tree(build_gradle_location)
251+
# Parse `parse_tree` for dependencies and print them
252+
return get_dependencies_from_parse_tree(parse_tree)
253+
254+
255+
def build_package(cls, dependencies):
256+
package_dependencies = []
257+
for dependency in dependencies:
258+
# Ignore collected dependencies that do not have a name
259+
name = dependency.get('name', '')
260+
if not name:
261+
continue
262+
263+
namespace = dependency.get('namespace', '')
264+
version = dependency.get('version', '')
265+
scope = dependency.get('scope', '')
266+
is_runtime = True
267+
is_optional = False
268+
if 'test' in scope.lower():
269+
is_runtime = False
270+
is_optional = True
271+
272+
package_dependencies.append(
273+
models.DependentPackage(
274+
purl=PackageURL(
275+
type='build.gradle',
276+
namespace=namespace,
277+
name=name,
278+
version=version
279+
).to_string(),
280+
scope=scope,
281+
requirement=version,
282+
is_runtime=is_runtime,
283+
is_optional=is_optional,
284+
)
285+
)
286+
287+
yield cls(
288+
dependencies=package_dependencies,
289+
)
290+
291+
292+
@attr.s()
293+
class BuildGradle(BaseBuildManifestPackage, models.PackageManifest):
294+
file_patterns = ('build.gradle',)
295+
extensions = ('.gradle',)
296+
# TODO: Not sure what the default type should be, change this to something
297+
# more appropriate later
298+
default_type = 'build.gradle'
299+
300+
@classmethod
301+
def recognize(cls, location):
302+
if not cls.is_manifest(location):
303+
return
304+
dependencies = get_dependencies(location)
305+
return build_package(cls, dependencies)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dependencies {
2+
// DEPENDENCY-5
3+
api dependencies.lombok
4+
5+
// DEPENDENCY-4
6+
api group: 'com.google', name: 'guava', version: '1.0'
7+
8+
// DEPENDENCY-4
9+
usageDependencies group: 'org.apache', name: 'commons', version: '1.0'
10+
11+
// DEPENDENCY-1
12+
runtimeOnly(
13+
[group: 'org.jacoco', name: 'org.jacoco.ant', version: '0.7.4.201502262128', classifier: "nodeps", transitive: false],
14+
[group: 'org.jacoco', name: 'org.jacoco.agent', version: '0.7.4.201502262128', classifier: "runtime", transitive: false]
15+
)
16+
}

0 commit comments

Comments
 (0)