Skip to content

Commit b131074

Browse files
committed
Adopt new handler design for Windows packages
We now have these data file handlers: * MsiInstallerHandler * NugetNupkgHandler * NugetNuspecHandler * WindowsExecutableHandler * MicrosoftUpdateManifestHandler * InstalledProgramFromDockerSoftwareDeltaHandler * InstalledProgramFromDockerFilesSoftwareHandler * InstalledProgramFromDockerUtilityvmSoftwareHandler And we assemble these in Packages correctly Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 2ad6761 commit b131074

36 files changed

+1713
-775
lines changed

src/packagedcode/msi.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@
1212
import warnings
1313
from shutil import which
1414

15-
import attr
16-
1715
from commoncode.command import execute
1816
from commoncode.command import find_in_path
1917
from commoncode.system import on_linux
2018
from commoncode.version import VERSION_PATTERNS_REGEX
2119
from packagedcode import models
2220

23-
2421
MSIINFO_BIN_LOCATION = 'packagedcode_msitools.msiinfo'
2522

2623

@@ -91,16 +88,16 @@ def get_msi_info(location):
9188
installing the `packagedcode-msiinfo` plugin or by installing `msitools`
9289
through a package manager.
9390
"""
91+
# FIXME: what about the return code? we ignore it?
9492
rc, stdout, stderr = execute(
9593
cmd_loc=get_msiinfo_bin_location(),
9694
args=[
9795
'suminfo',
9896
location,
9997
],
10098
)
101-
if stderr:
102-
error_message = f'Error encountered when reading MSI information from {location}: '
103-
error_message = error_message + stderr
99+
if stderr or rc:
100+
error_message = f'Error encountered when reading MSI information from {location}: {stderr}'
104101
raise MsiinfoException(error_message)
105102
return parse_msiinfo_suminfo_output(stdout)
106103

@@ -123,11 +120,15 @@ def get_version_from_subject_line(subject_line):
123120
return v
124121

125122

126-
def create_package_from_msiinfo_results(msiinfo_results):
123+
def create_package_data_from_msiinfo_results(
124+
msiinfo_results,
125+
datasource_id='msi_installer',
126+
package_type='msi',
127+
):
127128
"""
128-
Return an MsiInstallerPackage from the dictionary `msiinfo_results`
129+
Return PackageData from a mapping of `msiinfo_results`
129130
"""
130-
author_name = msiinfo_results.get('Author', '')
131+
author_name = msiinfo_results.pop('Author', '')
131132
parties = []
132133
if author_name:
133134
parties.append(
@@ -143,13 +144,15 @@ def create_package_from_msiinfo_results(msiinfo_results):
143144
# the time. Getting the version out of the `Subject` string is not
144145
# straightforward because the format of the string is usually different
145146
# between different MSIs
146-
subject = msiinfo_results.get('Subject', '')
147+
subject = msiinfo_results.pop('Subject', '')
147148
name = subject
148149
version = get_version_from_subject_line(subject)
149-
description = msiinfo_results.get('Comments', '')
150-
keywords = msiinfo_results.get('Keywords', '')
150+
description = msiinfo_results.pop('Comments', '')
151+
keywords = msiinfo_results.pop('Keywords', [])
151152

152-
return MsiInstallerPackage(
153+
return models.PackageData(
154+
datasource_id=datasource_id,
155+
type=package_type,
153156
name=name,
154157
version=version,
155158
description=description,
@@ -159,22 +162,35 @@ def create_package_from_msiinfo_results(msiinfo_results):
159162
)
160163

161164

162-
def msi_parse(location):
165+
def msi_parse(location,
166+
datasource_id='msi_installer',
167+
package_type='msi',
168+
):
163169
"""
164-
Return an MsiInstallerPackage from `location`
170+
Return PackageData from ``location``
165171
"""
166-
info = get_msi_info(location)
167-
return create_package_from_msiinfo_results(info)
172+
if on_linux:
173+
info = get_msi_info(location)
174+
return create_package_data_from_msiinfo_results(
175+
msiinfo_results=info,
176+
datasource_id=datasource_id,
177+
package_type=package_type,
178+
)
179+
else:
180+
return models.PackageData(
181+
datasource_id=datasource_id,
182+
type=package_type,
183+
)
168184

169185

170-
@attr.s()
171-
class MsiInstallerPackage(models.PackageData, models.PackageDataFile):
186+
class MsiInstallerHandler(models.DatafileHandler):
187+
datasource_id = 'msi_installer'
172188
filetypes = ('msi installer',)
173-
mimetypes = ('application/x-msi',)
174-
extensions = ('.msi',)
175-
default_type = 'msi'
189+
path_patterns = ('*.msi',)
190+
default_package_type = 'msi'
191+
description = 'Microsoft MSI installer'
192+
documentation_url = 'https://docs.microsoft.com/en-us/windows/win32/msi/windows-installer-portal'
176193

177194
@classmethod
178-
def recognize(cls, location):
179-
if on_linux:
180-
yield msi_parse(location)
195+
def parse(cls, location):
196+
yield msi_parse(location)

src/packagedcode/nuget.py

Lines changed: 40 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -7,127 +7,58 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import attr
1110
import xmltodict
1211

13-
from commoncode import filetype
1412
from packagedcode import models
1513
from packagedcode.utils import build_description
1614

17-
18-
# TODO: add dependencies
19-
2015
"""
21-
Handle nuget.org Nuget packages.
16+
Handle NuGet packages and their manifests.
2217
"""
18+
# TODO: add dependencies
2319

24-
# Tracing flags
25-
TRACE = False
26-
27-
28-
def logger_debug(*args):
29-
pass
30-
31-
32-
if TRACE:
33-
import logging
34-
import sys
35-
logger = logging.getLogger(__name__)
36-
# logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
37-
logging.basicConfig(stream=sys.stdout)
38-
logger.setLevel(logging.DEBUG)
3920

40-
def logger_debug(*args):
41-
return logger.debug(' '.join(isinstance(a, str) and a or repr(a) for a in args))
21+
def get_urls(name, version):
22+
return dict(
23+
repository_homepage_url=f'https://www.nuget.org/packages/{name}/{version}',
24+
repository_download_url=f'https://www.nuget.org/api/v2/package/{name}/{version}',
25+
# the name is lowercased as in
26+
# https://api.nuget.org/v3/registration3/newtonsoft.json/10.0.1.json
27+
api_data_url=f'https://api.nuget.org/v3/registration3/{name.lower()}/{version}.json',
28+
)
4229

4330

44-
@attr.s()
45-
class NugetPackageData(models.PackageData):
46-
# file_patterns = ('[Content_Types].xml', '*.nuspec',)
31+
class NugetNupkgHandler(models.NonAssemblableDatafileHandler):
32+
datasource_id = 'nuget_nupkg'
33+
path_patterns = ('*.nupkg',)
34+
default_package_type = 'nuget'
4735
filetypes = ('zip archive', 'microsoft ooxml',)
48-
mimetypes = ('application/zip', 'application/octet-stream',)
49-
# extensions = ('.nupkg',)
50-
51-
default_type = 'nuget'
52-
default_web_baseurl = 'https://www.nuget.org/packages/'
53-
default_download_baseurl = 'https://www.nuget.org/api/v2/package/'
54-
default_api_baseurl = 'https://api.nuget.org/v3/registration3/'
36+
description = 'NuGet nupkg package archive'
37+
documentation_url = 'https://en.wikipedia.org/wiki/Open_Packaging_Conventions'
5538

56-
@classmethod
57-
def get_package_root(cls, manifest_resource, codebase):
58-
if manifest_resource.name.endswith('.nupkg'):
59-
return manifest_resource
60-
if manifest_resource.name.endswith(('[Content_Types].xml', '.nuspec',)):
61-
return manifest_resource.parent(codebase)
62-
return manifest_resource
63-
64-
def repository_homepage_url(self, baseurl=default_web_baseurl):
65-
return baseurl + '{name}/{version}'.format(
66-
name=self.name, version=self.version)
67-
68-
def repository_download_url(self, baseurl=default_download_baseurl):
69-
return baseurl + '{name}/{version}'.format(
70-
name=self.name, version=self.version)
71-
72-
def api_data_url(self, baseurl=default_api_baseurl):
73-
# the name is lowercased
74-
# https://api.nuget.org/v3/registration3/newtonsoft.json/10.0.1.json
75-
return baseurl + '{name}/{version}.json'.format(
76-
name=self.name.lower(), version=self.version)
77-
78-
79-
nuspec_tags = [
80-
'id',
81-
'version',
82-
'title',
83-
'authors',
84-
'owners',
85-
'licenseUrl',
86-
'projectUrl',
87-
'requireLicenseAcceptance',
88-
'description',
89-
'summary',
90-
'releaseNotes',
91-
'copyright',
92-
'repository/@type',
93-
'repository/@url',
94-
]
95-
96-
97-
@attr.s()
98-
class Nuspec(NugetPackageData, models.PackageDataFile):
99-
100-
file_patterns = ('*.nuspec',)
101-
extensions = ('.nuspec',)
10239

103-
@classmethod
104-
def is_package_data_file(cls, location):
105-
"""
106-
Return True if the file at ``location`` is likely a manifest of this type.
107-
"""
108-
return filetype.is_file(location) and location.endswith('.nuspec')
40+
class NugetNuspecHandler(models.DatafileHandler):
41+
datasource_id = 'nuget_nupsec'
42+
path_patterns = ('*.nuspec',)
43+
default_package_type = 'nuget'
44+
description = 'NuGet nuspec package manifest'
45+
documentation_url = 'https://docs.microsoft.com/en-us/nuget/reference/nuspec'
10946

11047
@classmethod
111-
def recognize(cls, location):
112-
"""
113-
Yield one or more Package manifest objects given a file ``location`` pointing to a
114-
package archive, manifest or similar.
115-
"""
48+
def parse(cls, location):
11649
with open(location , 'rb') as loc:
11750
parsed = xmltodict.parse(loc)
11851

119-
if TRACE:
120-
logger_debug('parsed:', parsed)
12152
if not parsed:
12253
return
12354

124-
pack = parsed.get('package', {}) or {}
55+
pack = parsed.get('package') or {}
12556
nuspec = pack.get('metadata')
12657
if not nuspec:
12758
return
12859

129-
name=nuspec.get('id')
130-
version=nuspec.get('version')
60+
name = nuspec.get('id')
61+
version = nuspec.get('version')
13162

13263
# Summary: A short description of the package for UI display. If omitted, a
13364
# truncated version of description is used.
@@ -149,37 +80,35 @@ def recognize(cls, location):
14980
if owners:
15081
parties.append(models.Party(name=owners, role='owner'))
15182

83+
vcs_url = None
84+
15285
repo = nuspec.get('repository') or {}
153-
vcs_tool = repo.get('@type') or ''
15486
vcs_repository = repo.get('@url') or ''
155-
vcs_url =None
15687
if vcs_repository:
88+
vcs_tool = repo.get('@type') or ''
15789
if vcs_tool:
158-
vcs_url = '{}+{}'.format(vcs_tool, vcs_repository)
90+
vcs_url = f'{vcs_tool}+{vcs_repository}'
15991
else:
16092
vcs_url = vcs_repository
16193

162-
yield cls(
94+
urls = get_urls(name, version)
95+
96+
package_data = models.PackageData(
97+
datasource_id=cls.datasource_id,
98+
type=cls.default_package_type,
16399
name=name,
164100
version=version,
165101
description=description or None,
166102
homepage_url=nuspec.get('projectUrl') or None,
167103
parties=parties,
104+
# FIXME: license has evolved and is now SPDX...
168105
declared_license=nuspec.get('licenseUrl') or None,
169106
copyright=nuspec.get('copyright') or None,
170107
vcs_url=vcs_url,
108+
**urls,
171109
)
172110

111+
if not package_data.license_expression and package_data.declared_license:
112+
package_data.license_expression = cls.compute_normalized_license(package_data)
173113

174-
@attr.s()
175-
class NugetPackage(NugetPackageData, models.Package):
176-
"""
177-
A Nuget Package that is created out of one/multiple nuget package
178-
manifests.
179-
"""
180-
181-
@property
182-
def manifests(self):
183-
return [
184-
Nuspec
185-
]
114+
yield package_data

0 commit comments

Comments
 (0)