diff --git a/src/packagedcode/__init__.py b/src/packagedcode/__init__.py index 57737a406c7..91efee0424f 100644 --- a/src/packagedcode/__init__.py +++ b/src/packagedcode/__init__.py @@ -25,16 +25,17 @@ from __future__ import absolute_import from __future__ import unicode_literals -from packagedcode import build -from packagedcode import chef -from packagedcode import models from packagedcode import about from packagedcode import bower -from packagedcode import conda +from packagedcode import build from packagedcode import cargo +from packagedcode import chef +from packagedcode import cocoapods +from packagedcode import conda from packagedcode import freebsd from packagedcode import haxe from packagedcode import maven +from packagedcode import models from packagedcode import npm from packagedcode import nuget from packagedcode import phpcomposer @@ -62,6 +63,7 @@ phpcomposer.PHPComposerPackage, haxe.HaxePackage, cargo.RustCargoCrate, + cocoapods.CocoapodsPackage, models.MeteorPackage, bower.BowerPackage, freebsd.FreeBSDPackage, diff --git a/src/packagedcode/cocoapods.py b/src/packagedcode/cocoapods.py new file mode 100644 index 00000000000..f5cd0da6738 --- /dev/null +++ b/src/packagedcode/cocoapods.py @@ -0,0 +1,183 @@ +# All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import re + +import attr +from packageurl import PackageURL + +from commoncode.fileutils import py2 +from commoncode import filetype +from commoncode import fileutils +from packagedcode import models +from packagedcode import spec + + +""" +Handle cocoapods packages manifests for macOS and iOS +including .podspec, Podfile and Podfile.lock files. +See https://cocoapods.org +""" + +# TODO: implementation to get dependency data using gemsfileparser +# Check: https://gitlab.com/balasankarc/gemfileparser +# TODO: override the license detection to detect declared_license correctly. + + +TRACE = False + +logger = logging.getLogger(__name__) + +if TRACE: + import sys + logging.basicConfig(stream=sys.stdout) + logger.setLevel(logging.DEBUG) + + +@attr.s() +class CocoapodsPackage(models.Package): + metafiles = ('*.podspec',) + extensions = ('.podspec',) + default_type = 'pods' + default_primary_language = 'Objective-C' + default_web_baseurl = 'https://cocoapods.org' + default_download_baseurl = None + default_api_baseurl = None + + @classmethod + def recognize(cls, location): + yield parse(location) + + def repository_homepage_url(self, baseurl=default_web_baseurl): + return '{}/pods/{}'.format(baseurl, self.name) + + def repository_download_url(self): + return '{}/archive/{}.zip'.format(self.homepage_url, self.version) + + +def is_podspec(location): + """ + Checks is the file is a podspec file or not. + """ + return (filetype.is_file(location) and location.endswith('.podspec')) + + +def parse(location): + """ + Return a Package object from a .podspec file or None. + """ + if not is_podspec(location): + return + + podspec_data = spec.parse_spec(location) + return build_package(podspec_data) + + +def build_package(podspec_data): + """ + Return a Pacakge object from a package data mapping or None. + """ + name = podspec_data.get('name') + version = podspec_data.get('version') + declared_license = podspec_data.get('license') + summary = podspec_data.get('summary') + description = podspec_data.get('description') + homepage_url = podspec_data.get('homepage_url') + source = podspec_data.get('source') + authors = podspec_data.get('author') or None + + author_names = [] + author_email = [] + if authors: + for split_author in authors: + split_author = split_author.strip() + author, email = parse_person(split_author) + author_names.append(author) + author_email.append(email) + + parties = [] + if authors: + parties.append( + models.Party( + type=models.party_person, + name=', '.join(author_names), + email=', '.join(author_email), + role='author' + ) + ) + + if len(summary) > len(description): + description = summary + + package = CocoapodsPackage( + name=name, + version=version, + vcs_url=source, + source_packages=list(source.split('\n')), + description=description, + declared_license=declared_license, + homepage_url=homepage_url, + parties=parties + ) + return package + + +person_parser = re.compile( + r'^(?P[\w\s(),-_.,]+)' + r'=>' + r'(?P[\S+]+$)' +).match + +person_parser_only_name = re.compile( + r'^(?P[\w\s(),-_.,]+)' +).match + + +def parse_person(person): + """ + https://guides.cocoapods.org/syntax/podspec.html#authors + Author can be in the form: + s.author = 'Rohit Potter' + or + s.author = 'Rohit Potter=>rohit@gmail.com' + Author check: + >>> p = parse_person('Rohit Potter=>rohit@gmail.com') + >>> assert p == ('Rohit Potter', 'rohit@gmail.com') + >>> p = parse_person('Rohit Potter') + >>> assert p == ('Rohit Potter', None) + """ + parsed = person_parser(person) + if not parsed: + parsed = person_parser_only_name(person) + name = parsed.group('name') + email = None + else: + name = parsed.group('name') + email = parsed.group('email') + + return name, email \ No newline at end of file diff --git a/src/packagedcode/spec.py b/src/packagedcode/spec.py new file mode 100644 index 00000000000..04b3b757407 --- /dev/null +++ b/src/packagedcode/spec.py @@ -0,0 +1,118 @@ +# All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + + +import csv +import glob +import io +import os +import re + + +""" +Handle Cocoapods(.podspec) and Ruby(.gemspec) files. +""" + + +def get_stripped_data(line): + """ + Return data after removing unnecessary special character and space. + """ + if '#' in line: + line = line[:line.index('#')] + stripped_data = line.strip() + stripped_data = stripped_data.replace("'", '') + stripped_data = stripped_data.replace('"', '') + stripped_data = stripped_data.replace('{', '') + stripped_data = stripped_data.replace('}', '') + stripped_data = stripped_data.strip() + return stripped_data + + +def get_description(location): + """ + https://guides.cocoapods.org/syntax/podspec.html#description + description is in the form: + spec.description = <<-DESC + Computes the meaning of life. + Features: + 1. Is self aware + ... + 42. Likes candies. + DESC + Return description from podspec. + """ + with io.open(location, encoding='utf-8', closefd=True) as data: + lines = data.readlines() + description = '' + for i, content in enumerate(lines): + if '.description' in content: + for cont in lines[i+1:]: + if 'DESC' in cont: + break + description += ' '.join([description, cont.strip()]) + break + description.strip() + return description + + +def parse_spec(location): + """ + Return dictionary contains podspec or gemspec file data. + """ + with io.open(location, encoding='utf-8', closefd=True) as data: + lines = data.readlines() + + spec_data = {} + + for line in lines: + if '.name' in line: + name = re.sub(r'/*.*name.*?=', '', line) + spec_data['name'] = get_stripped_data(name) + elif '.version' in line and '.version.' not in line: + version = re.sub(r'/*.*version.*?=', '', line) + spec_data['version'] = get_stripped_data(version) + elif '.license' in line: + license_type = re.sub(r'/*.*license.*?=', '', line) + spec_data['license'] = get_stripped_data(license_type) + elif '.source' in line and '.source_files' not in line: + source = re.sub(r'/*.*source.*?>', '', line) + stripped_source = re.sub(r',.*', '', source) + spec_data['source'] = get_stripped_data(stripped_source) + elif '.author' in line: + authors = re.sub(r'/*.*author.*?=', '', line) + stripped_authors = get_stripped_data(authors) + stripped_authors = stripped_authors.replace(' => ', "=>") + stripped_authors = stripped_authors.strip() + stripped_authors = stripped_authors.split(',') + spec_data['author'] = stripped_authors + elif '.summary' in line: + summary = re.sub(r'/*.*summary.*?=', '', line) + spec_data['summary'] = get_stripped_data(summary) + elif '.description' in line: + spec_data['description'] = get_description(location) + elif '.homepage' in line: + homepage_url = re.sub(r'/*.*homepage.*?=', '', line) + spec_data['homepage_url'] = get_stripped_data(homepage_url) + + return spec_data \ No newline at end of file diff --git a/tests/packagedcode/data/plugin/help.txt b/tests/packagedcode/data/plugin/help.txt index ae70e508d32..eac2cdbee60 100644 --- a/tests/packagedcode/data/plugin/help.txt +++ b/tests/packagedcode/data/plugin/help.txt @@ -200,6 +200,12 @@ Package: nuget extensions: .nupkg filetypes: zip archive, microsoft ooxml +-------------------------------------------- +Package: pods + class: packagedcode.cocoapods:CocoapodsPackage + metafiles: *.podspec + extensions: .podspec + -------------------------------------------- Package: pypi class: packagedcode.pypi:PythonPackage