Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/build/sage_bootstrap/package.py
4052 views
1
# -*- coding: utf-8 -*-
2
"""
3
Sage Packages
4
"""
5
6
# ****************************************************************************
7
# Copyright (C) 2015-2016 Volker Braun <[email protected]>
8
# 2018 Jeroen Demeyer
9
# 2020-2024 Matthias Koeppe
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 2 of the License, or
14
# (at your option) any later version.
15
# https://www.gnu.org/licenses/
16
# ****************************************************************************
17
18
import logging
19
import os
20
import re
21
22
from sage_bootstrap.env import SAGE_ROOT
23
24
log = logging.getLogger()
25
26
27
class Package(object):
28
29
def __new__(cls, package_name):
30
if package_name.startswith("pypi/") or package_name.startswith("generic/"):
31
package_name = "pkg:" + package_name
32
if package_name.startswith("pkg:"):
33
package_name = package_name.replace('_', '-').lower()
34
if package_name.startswith("pkg:generic/"): # fast path
35
try:
36
pkg = cls(package_name[len("pkg:generic/"):].replace('-', '_'))
37
if pkg.purl == package_name:
38
return pkg # assume unique
39
except Exception:
40
pass
41
elif package_name.startswith("pkg:pypi/"): # fast path
42
try:
43
pkg = cls(package_name[len("pkg:pypi/"):].replace('-', '_'))
44
if pkg.purl == package_name:
45
return pkg # assume unique
46
except Exception:
47
pass
48
for pkg in cls.all():
49
if pkg.purl == package_name:
50
return pkg # assume unique
51
raise ValueError('no package for PURL {0}'.format(package_name))
52
self = object.__new__(cls)
53
self.__init__(package_name)
54
return self
55
56
def __init__(self, package_name):
57
"""
58
Sage Package
59
60
A package is defined by a subdirectory of
61
``SAGE_ROOT/build/pkgs/``. The name of the package is the name
62
of the subdirectory; The metadata of the package is contained
63
in various files in the package directory. This class provides
64
an abstraction to the metadata, you should never need to
65
access the package directory directly.
66
67
INPUT:
68
69
-- ``package_name`` -- string. Name of the package. The Sage
70
convention is that all package names are lower case.
71
"""
72
if any(package_name.startswith(prefix)
73
for prefix in ["pkg:", "pypi/", "generic"]):
74
# Already initialized
75
return
76
if package_name != package_name.lower():
77
raise ValueError('package names should be lowercase, got {0}'.format(package_name))
78
if '-' in package_name:
79
raise ValueError('package names use underscores, not dashes, got {0}'.format(package_name))
80
81
self.__name = package_name
82
self.__tarball = None
83
self._init_checksum()
84
self._init_version()
85
self._init_type()
86
self._init_version_requirements()
87
self._init_requirements()
88
self._init_dependencies()
89
self._init_trees()
90
91
def __repr__(self):
92
return 'Package {0}'.format(self.name)
93
94
@property
95
def name(self):
96
"""
97
Return the package name
98
99
A package is defined by a subdirectory of
100
``SAGE_ROOT/build/pkgs/``. The name of the package is the name
101
of the subdirectory.
102
103
OUTPUT:
104
105
String.
106
"""
107
return self.__name
108
109
@property
110
def sha1(self):
111
"""
112
Return the SHA1 checksum
113
114
OUTPUT:
115
116
String.
117
"""
118
return self.__sha1
119
120
@property
121
def sha256(self):
122
"""
123
Return the SHA256 checksum
124
125
OUTPUT:
126
127
String.
128
"""
129
return self.__sha256
130
131
@property
132
def tarball(self):
133
"""
134
Return the (primary) tarball
135
136
If there are multiple tarballs (currently unsupported), this
137
property returns the one that is unpacked automatically.
138
139
OUTPUT:
140
141
Instance of :class:`sage_bootstrap.tarball.Tarball`
142
"""
143
if self.__tarball is None:
144
from sage_bootstrap.tarball import Tarball
145
self.__tarball = Tarball(self.tarball_filename, package=self)
146
return self.__tarball
147
148
def _substitute_variables_once(self, pattern):
149
"""
150
Substitute (at most) one occurrence of variables in ``pattern`` by the values.
151
152
These variables are ``VERSION``, ``VERSION_MAJOR``, ``VERSION_MINOR``,
153
``VERSION_MICRO``, either appearing like this or in the form ``${VERSION_MAJOR}``
154
etc.
155
156
Return a tuple:
157
- the string with the substitution done or the original string
158
- whether a substitution was done
159
"""
160
for var in ('VERSION_MAJOR', 'VERSION_MINOR', 'VERSION_MICRO', 'VERSION'):
161
# As VERSION is a substring of the other three, it needs to be tested last.
162
dollar_brace_var = '${' + var + '}'
163
if dollar_brace_var in pattern:
164
value = getattr(self, var.lower())
165
return pattern.replace(dollar_brace_var, value, 1), True
166
elif var in pattern:
167
value = getattr(self, var.lower())
168
return pattern.replace(var, value, 1), True
169
return pattern, False
170
171
def _substitute_variables(self, pattern):
172
"""
173
Substitute all occurrences of ``VERSION`` in ``pattern`` by the actual version.
174
175
Likewise for ``VERSION_MAJOR``, ``VERSION_MINOR``, ``VERSION_MICRO``,
176
either appearing like this or in the form ``${VERSION}``, ``${VERSION_MAJOR}``,
177
etc.
178
"""
179
not_done = True
180
while not_done:
181
pattern, not_done = self._substitute_variables_once(pattern)
182
return pattern
183
184
@property
185
def tarball_pattern(self):
186
"""
187
Return the (primary) tarball file pattern
188
189
If there are multiple tarballs (currently unsupported), this
190
property returns the one that is unpacked automatically.
191
192
OUTPUT:
193
194
String. The full-qualified tarball filename, but with
195
``VERSION`` instead of the actual tarball filename.
196
"""
197
return self.__tarball_pattern
198
199
@property
200
def tarball_filename(self):
201
"""
202
Return the (primary) tarball filename
203
204
If there are multiple tarballs (currently unsupported), this
205
property returns the one that is unpacked automatically.
206
207
OUTPUT:
208
209
String. The full-qualified tarball filename.
210
"""
211
pattern = self.tarball_pattern
212
if pattern:
213
return self._substitute_variables(pattern)
214
else:
215
return None
216
217
@property
218
def tarball_upstream_url_pattern(self):
219
"""
220
Return the tarball upstream URL pattern
221
222
OUTPUT:
223
224
String. The tarball upstream URL, but with the placeholder
225
``VERSION``.
226
"""
227
return self.__tarball_upstream_url_pattern
228
229
@property
230
def tarball_upstream_url(self):
231
"""
232
Return the tarball upstream URL or ``None`` if none is recorded
233
234
OUTPUT:
235
236
String. The URL.
237
"""
238
pattern = self.tarball_upstream_url_pattern
239
if pattern:
240
return self._substitute_variables(pattern)
241
else:
242
return None
243
244
@property
245
def tarball_package(self):
246
"""
247
Return the canonical package for the tarball
248
249
This is almost always equal to ``self`` except if the package
250
or the ``checksums.ini`` file is a symbolic link. In that case,
251
the package of the symbolic link is returned.
252
253
OUTPUT:
254
255
A ``Package`` instance
256
"""
257
n = self.__tarball_package_name
258
if n == self.name:
259
return self
260
else:
261
return type(self)(n)
262
263
@property
264
def version(self):
265
"""
266
Return the version
267
268
OUTPUT:
269
270
String. The package version. Excludes the Sage-specific
271
patchlevel.
272
"""
273
return self.__version
274
275
@property
276
def version_major(self):
277
"""
278
Return the major version
279
280
OUTPUT:
281
282
String. The package's major version.
283
"""
284
return self.version.split('.')[0]
285
286
@property
287
def version_minor(self):
288
"""
289
Return the minor version
290
291
OUTPUT:
292
293
String. The package's minor version.
294
"""
295
return self.version.split('.')[1]
296
297
@property
298
def version_micro(self):
299
"""
300
Return the micro version
301
302
OUTPUT:
303
304
String. The package's micro version.
305
"""
306
return self.version.split('.')[2]
307
308
@property
309
def patchlevel(self):
310
"""
311
Return the patchlevel
312
313
OUTPUT:
314
315
Integer. The patchlevel of the package. Excludes the "p"
316
prefix.
317
"""
318
return self.__patchlevel
319
320
@property
321
def version_with_patchlevel(self):
322
"""
323
Return the version, including the Sage-specific patchlevel
324
325
OUTPUT:
326
327
String.
328
"""
329
v = self.version
330
if v is None:
331
return v
332
p = self.patchlevel
333
if p < 0:
334
return v
335
return "{0}.p{1}".format(v, p)
336
337
@property
338
def type(self):
339
"""
340
Return the package type
341
"""
342
return self.__type
343
344
@property
345
def source(self):
346
"""
347
Return the package source type
348
"""
349
if self.__requirements is not None:
350
return 'pip'
351
if self.tarball_filename:
352
if self.tarball_filename.endswith('.whl'):
353
return 'wheel'
354
return 'normal'
355
if self.has_file('spkg-install') or self.has_file('spkg-install.in'):
356
return 'script'
357
return 'none'
358
359
@property
360
def trees(self):
361
"""
362
Return the installation trees for the package
363
364
OUTPUT:
365
366
A white-space-separated string of environment variable names
367
"""
368
if self.__trees is not None:
369
return self.__trees
370
if self.__version_requirements is not None:
371
return 'SAGE_VENV'
372
if self.__requirements is not None:
373
return 'SAGE_VENV'
374
return 'SAGE_LOCAL'
375
376
@property
377
def purl(self):
378
"""
379
Return a PURL (Package URL) for the package
380
381
OUTPUT:
382
383
A string in the format ``SCHEME:TYPE/NAMESPACE/NAME``,
384
i.e., without components for version, qualifiers, and subpath.
385
See https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#package-url-specification-v10x
386
for details
387
"""
388
dist = self.distribution_name
389
if dist:
390
return 'pkg:pypi/' + dist.lower().replace('_', '-')
391
return 'pkg:generic/' + self.name.replace('_', '-')
392
393
@property
394
def distribution_name(self):
395
"""
396
Return the Python distribution name or ``None`` for non-Python packages
397
"""
398
if self.__requirements is not None:
399
for line in self.__requirements.split('\n'):
400
line = line.strip()
401
if line.startswith('#'):
402
continue
403
for part in line.split():
404
return part
405
if self.__version_requirements is None:
406
return None
407
for line in self.__version_requirements.split('\n'):
408
line = line.strip()
409
if line.startswith('#'):
410
continue
411
for part in line.split():
412
return part
413
return None
414
415
@property
416
def dependencies(self):
417
"""
418
Return a list of strings, the package names of the (ordinary) dependencies
419
"""
420
# after a '|', we have order-only dependencies
421
return self.__dependencies.partition('|')[0].strip().split()
422
423
@property
424
def dependencies_order_only(self):
425
"""
426
Return a list of strings, the package names of the order-only dependencies
427
"""
428
return self.__dependencies.partition('|')[2].strip().split() + self.__dependencies_order_only.strip().split()
429
430
@property
431
def dependencies_optional(self):
432
"""
433
Return a list of strings, the package names of the optional build dependencies
434
"""
435
return self.__dependencies_optional.strip().split()
436
437
@property
438
def dependencies_runtime(self):
439
"""
440
Return a list of strings, the package names of the runtime dependencies
441
"""
442
# after a '|', we have order-only build dependencies
443
return self.__dependencies.partition('|')[0].strip().split()
444
445
@property
446
def dependencies_check(self):
447
"""
448
Return a list of strings, the package names of the check dependencies
449
"""
450
return self.__dependencies_order_only.strip().split()
451
452
def __eq__(self, other):
453
return self.tarball == other.tarball
454
455
@classmethod
456
def all(cls):
457
"""
458
Return all packages
459
"""
460
base = os.path.join(SAGE_ROOT, 'build', 'pkgs')
461
for subdir in os.listdir(base):
462
path = os.path.join(base, subdir)
463
if not os.path.isfile(os.path.join(path, "type")):
464
log.debug('%s has no type', subdir)
465
continue
466
try:
467
yield cls(subdir)
468
except Exception:
469
log.error('Failed to open %s', subdir)
470
raise
471
472
@property
473
def path(self):
474
"""
475
Return the package directory
476
"""
477
return os.path.join(SAGE_ROOT, 'build', 'pkgs', self.name)
478
479
def has_file(self, filename):
480
"""
481
Return whether the file exists in the package directory
482
"""
483
return os.path.exists(os.path.join(self.path, filename))
484
485
def line_count_file(self, filename):
486
"""
487
Return the number of lines of the file
488
489
Directories are traversed recursively.
490
491
OUTPUT:
492
493
integer; 0 if the file cannot be read, 1 if it is a symlink
494
"""
495
path = os.path.join(self.path, filename)
496
if os.path.islink(path):
497
return 1
498
if os.path.isdir(path):
499
return sum(self.line_count_file(os.path.join(filename, entry))
500
for entry in os.listdir(path))
501
try:
502
with open(path, "rb") as f:
503
return len(list(f))
504
except OSError:
505
return 0
506
507
def _init_checksum(self):
508
"""
509
Load the checksums from the appropriate ``checksums.ini`` file
510
"""
511
checksums_ini = os.path.join(self.path, 'checksums.ini')
512
assignment = re.compile('(?P<var>[a-zA-Z0-9_]*)=(?P<value>.*)')
513
result = dict()
514
try:
515
with open(checksums_ini, 'rt') as f:
516
for line in f.readlines():
517
match = assignment.match(line)
518
if match is None:
519
continue
520
var, value = match.groups()
521
result[var] = value
522
except IOError:
523
pass
524
self.__sha1 = result.get('sha1', None)
525
self.__sha256 = result.get('sha256', None)
526
self.__tarball_pattern = result.get('tarball', None)
527
self.__tarball_upstream_url_pattern = result.get('upstream_url', None)
528
# Name of the directory containing the checksums.ini file
529
self.__tarball_package_name = os.path.realpath(checksums_ini).split(os.sep)[-2]
530
531
VERSION_PATCHLEVEL = re.compile(r'(?P<version>.*)\.p(?P<patchlevel>[0-9]+)')
532
533
def _init_version(self):
534
try:
535
with open(os.path.join(self.path, 'package-version.txt')) as f:
536
package_version = f.read().strip()
537
except IOError:
538
self.__version = None
539
self.__patchlevel = None
540
else:
541
match = self.VERSION_PATCHLEVEL.match(package_version)
542
if match is None:
543
self.__version = package_version
544
self.__patchlevel = -1
545
else:
546
self.__version = match.group('version')
547
self.__patchlevel = int(match.group('patchlevel'))
548
549
def _init_type(self):
550
with open(os.path.join(self.path, 'type')) as f:
551
package_type = f.read().strip()
552
assert package_type in [
553
'base', 'standard', 'optional', 'experimental'
554
]
555
self.__type = package_type
556
557
def _init_version_requirements(self):
558
try:
559
with open(os.path.join(self.path, 'version_requirements.txt')) as f:
560
self.__version_requirements = f.read().strip()
561
except IOError:
562
self.__version_requirements = None
563
564
def _init_requirements(self):
565
try:
566
with open(os.path.join(self.path, 'requirements.txt')) as f:
567
self.__requirements = f.read().strip()
568
except IOError:
569
self.__requirements = None
570
571
def _init_dependencies(self):
572
try:
573
with open(os.path.join(self.path, 'dependencies')) as f:
574
self.__dependencies = f.readline().partition('#')[0].strip()
575
except IOError:
576
self.__dependencies = ''
577
try:
578
with open(os.path.join(self.path, 'dependencies_check')) as f:
579
self.__dependencies_check = f.readline().partition('#')[0].strip()
580
except IOError:
581
self.__dependencies_check = ''
582
try:
583
with open(os.path.join(self.path, 'dependencies_optional')) as f:
584
self.__dependencies_optional = f.readline().partition('#')[0].strip()
585
except IOError:
586
self.__dependencies_optional = ''
587
try:
588
with open(os.path.join(self.path, 'dependencies_order_only')) as f:
589
self.__dependencies_order_only = f.readline().partition('#')[0].strip()
590
except IOError:
591
self.__dependencies_order_only = ''
592
593
def _init_trees(self):
594
try:
595
with open(os.path.join(self.path, 'trees.txt')) as f:
596
self.__trees = f.readline().partition('#')[0].strip()
597
except IOError:
598
self.__trees = None
599
600