-
-
Notifications
You must be signed in to change notification settings - Fork 562
/
alpine.py
1657 lines (1353 loc) · 55.2 KB
/
alpine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# ScanCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/nexB/scancode-toolkit for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import base64
import codecs
import email
import posixpath
import re
from functools import partial
from datetime import datetime
from os import path
import attr
from license_expression import LicenseSymbolLike
from license_expression import Licensing
from packageurl import PackageURL
from packagedcode import bashparse
from packagedcode import models
from packagedcode.utils import combine_expressions
from textcode.analysis import as_unicode
@attr.s()
class AlpinePackage(models.Package, models.PackageManifest):
extensions = ('.apk', 'APKBUILD')
default_type = 'alpine'
@classmethod
def recognize(cls, location):
return parse_apkbuild(location, strict=True)
def compute_normalized_license(self):
_declared, detected = detect_declared_license(self.declared_license)
return detected
def to_dict(self, _detailed=False, **kwargs):
data = models.Package.to_dict(self, **kwargs)
if _detailed:
#################################################
data['installed_files'] = [istf.to_dict() for istf in (self.installed_files or [])]
#################################################
else:
#################################################
# remove temporary fields
data.pop('installed_files', None)
#################################################
return data
def get_installed_packages(root_dir, **kwargs):
"""
Yield Package objects given a ``root_dir`` rootfs directory.
"""
installed_file_loc = path.join(root_dir, 'lib/apk/db/installed')
if not path.exists(installed_file_loc):
return
for package in parse_alpine_installed_db(installed_file_loc):
yield package
def parse_alpine_installed_db(location):
"""
Yield AlpinePackage objects from an installed database file at `location`
or None. Typically found at '/lib/apk/db/installed' in an Alpine
installation.
Note: http://uk.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz are
also in the same format as an installed database.
"""
for fields in get_alpine_installed_db_fields(location):
yield build_package(fields)
def get_alpine_installed_db_fields(location):
"""
Yield lists of (name, value) pairsfrom an installed database file at `location`
Typically found at '/lib/apk/db/installed' in an Alpine
installation.
Note: http://uk.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz are
also in the same format as an installed database.
"""
if not path.exists(location):
return
with open(location, 'rb') as f:
installed = as_unicode(f.read())
if installed:
# each installed package paragraph is separated by LFLF
packages = (p for p in re.split('\n\n', installed) if p)
for pkg in packages:
try:
fields = email.message_from_string(pkg)
except UnicodeEncodeError:
fields = email.message_from_string(pkg.encode('utf-8'))
yield [(n.strip(), v.strip(),) for n, v in fields.items()]
def is_apkbuild(location):
return location and location.endswith('APKBUILD')
# these variables need to be resolved or else this is a parsing error
# we also only use these variables
APKBUILD_VARIABLES = set([
'pkgname',
'pkgver',
'pkgrel',
'pkgdesc',
'license',
'url'
'arch',
# 'subpackages',
# depends
# 'depend',
# makedepends
'source',
'sha512sums',
'sha256sums',
'md5sums',
])
ESSENTIAL_APKBUILD_VARIABLES = set([
'pkgname',
'pkgver',
'license',
'url'
'arch',
'source',
'sha512sums',
'sha256sums',
'md5sums',
])
def parse_apkbuild(location, strict=False):
"""
Return an AlpinePackage object from an APKBUILD file at `location` or None.
"""
if not path.exists(location) or not is_apkbuild(location):
return
with open(location, 'rb') as f:
apkbuild = as_unicode(f.read())
return parse_apkbuild_text(text=apkbuild, strict=strict)
class ApkbuildParseFailure(Exception):
pass
def get_apkbuild_variables(text):
"""
Parse ``text`` and return a tuple of (variables, errors) where variables is
a list of bashparse.ShellVariables with a name listed in the ``names`` set.
"""
fixed_text = fix_apkbuild(text)
variables, errors = bashparse.collect_shell_variables_from_text(
text=fixed_text,
resolve=True,
needed_variables=APKBUILD_VARIABLES,
)
return variables, errors
def replace_fix(text, source, target, *args):
"""
Replace ``source`` by ``target`` in ``text`` and return ``text``.
"""
return text.replace(source, target)
def extract_var(text, varname):
"""
Return the a single line attribute with variable named ``varname `` found in
the ``text`` APKBUILD or an empty string.
For example::
>>> t='''
... pkgname=sqlite-tcl
... pkgrel=0
... '''
>>> extract_var(t, 'pkgname')
'sqlite-tcl'
"""
try:
variable = [
l for l in text.splitlines(False)
if l.startswith(f'{varname}=')
][0]
except Exception:
variable = ''
_, _, value = variable.partition('=')
return value
def extract_pkgver(text):
"""
Return the pkgver version attribute found in the ``text`` APKBUILD or an
empty string.
For example::
>>> t='''
... pkgname=sqlite-tcl
... pkgver=3.35.5
... pkgrel=0
... '''
>>> extract_pkgver(t)
'3.35.5'
"""
return extract_var(text, varname='pkgver')
def get_pkgver_substring(text, version_segments=2):
"""
Return a pkgver version substring from ``text`` APKBUILD using up to
``version_segments`` segments of the version (dot-separated).
"""
version = extract_pkgver(text)
# keep only first few segments using "version_segments"
segments = version.split('.')[:version_segments]
return '.'.join(segments)
def add_version_substring_variable(text, varname, version_segments=2):
"""
Fix things in ``text`` such as :
case $pkgver in
*.*.*) _kernver=${pkgver%.*};;
*.*) _kernver=$pkgver;;
esac
This is a common pattern in APKBUILD to extract some segments from a version
"""
version = get_pkgver_substring(text, version_segments=version_segments)
return f'{varname}={version} #fixed by scancode\n\n{text}'
def fix_sqsh_2_segments_version(text, *args):
"""
Fix this in ``text``:
pkgver=2.5.16.1
case $pkgver in
*.*.*.*) _v=${pkgver%.*.*};;
*.*.*) _v=${pkgver%.*};;
*) _v=$pkgver;;
esac
"""
return add_version_substring_variable(text, varname='_v', version_segments=2)
def fix_libreoffice_version(text, *args):
"""
Fix this in ``text``:
pkgname=libreoffice
pkgver=6.4.2
case $pkgver in
*.*.*.*) _v=${pkgver%.*};;
*.*.*) _v=$pkgver;;
esac
"""
return add_version_substring_variable(text, varname='_v', version_segments=3)
def fix_kernver_version(text, *args):
"""
Fix things in ``text`` such as :
case $pkgver in
*.*.*) _kernver=${pkgver%.*};;
*.*) _kernver=$pkgver;;
esac
and:
case $pkgver in
*.*.*) _kernver=${pkgver%.*};;
*.*) _kernver=${pkgver};;
esac
"""
return add_version_substring_variable(text, varname='_kernver', version_segments=2)
def fix_dunder_v_version(text, *args):
"""
Fix things in ``text`` such as :
case $pkgver in
*.*.*) _v=${pkgver%.*};;
*.*) _v=$pkgver;;
esac
"""
return add_version_substring_variable(text, varname='_v', version_segments=2)
def fix_kde_version(text, *args):
"""
Fix this in ``text``:
case "$pkgver" in
*.90*) _rel=unstable;;
*) _rel=stable;;
esac
"""
version = extract_pkgver(text)
if '.90' in version:
rel = 'unstable'
else:
rel = 'stable'
return f'_rel={rel} #fixed by scancode\n\n{text}'
def fix_sudo_realver(text, *args):
"""
sudo-1.9.6p1.tar.gz
pkgver=1.9.6_p1
if [ "${pkgver%_*}" != "$pkgver" ]; then
_realver=${pkgver%_*}${pkgver#*_}
else
_realver=$pkgver
fi
source="https://www.sudo.ws/dist/sudo-$_realver.tar.gz
"""
_realver = extract_pkgver(text).replace('_', '')
return f'_realver={_realver} #fixed by scancode\n\n{text}'
def convert_sqlite_version(version):
"""
Return a version converted from the sqlite human version format to the
download version format.
For instance:
>>> convert_sqlite_version('3.36.0')
'3360000'
>>> convert_sqlite_version('3.36.0.2')
'3360002'
See also: https://www.sqlite.org/versionnumbers.html
From https://www.sqlite.org/download.html
Build Product Names and Info
Build products are named using one of the following templates:
sqlite-product-version.zip
sqlite-product-date.zip
[...]
The version is encoded so that filenames sort in order of increasing
version number when viewed using "ls". For version 3.X.Y the filename
encoding is 3XXYY00. For branch version 3.X.Y.Z, the encoding is
3XXYYZZ.
The date in template (4) is of the form: YYYYMMDDHHMM
"""
segments = [int(s) for s in version.strip().split('.')]
if len(segments) == 3:
# all version should have 4 segments. Add it if missing
segments.append(0)
try:
major, minor, patch, branch = segments
except Exception:
raise Exception(
f'Unsupported sqlite version format. Should have 3 or 4 segments: '
f'{version}'
)
# pad each segments except the first on the left with zeroes 3XXYY00
return f'{major}{minor:>02d}{patch:>02d}{branch:>02d}'
def fix_sqlite_version(text, *args):
"""
Fix the complex SQLite version conversion.
See https://www.sqlite.org/download.html
case $pkgver in
*.*.*.*)_d=${pkgver##*.};;
*.*.*) _d=0;;
esac
"""
converted = convert_sqlite_version(extract_pkgver(text))
vers = '_ver=${_a}${_b}${_c}$_d\n'
text = text.replace(
vers,
f'{vers}'
f'_ver={converted} #fixed by scancode\n\n'
)
return text
def extract_lua__pkgname(text):
"""
For example:
>>> t='''
... pkgname=lua-unit
... foo=bar
... '''
>>> extract_lua__pkgname(t)
'LUAUNIT'
"""
return extract_var(text, varname='pkgname').replace('-', '').upper()
def fix_lua_pkgname(text, *args):
"""
Fix things in ``text`` such as:
pkgname=lua-unit
_pkgname=$(echo ${pkgname/-/} | tr '[:lower:]' '[:upper:]')
"""
converted = extract_lua__pkgname(text)
_pkgname = "_pkgname=$(echo ${pkgname/-/} | tr '[:lower:]' '[:upper:]')\n"
# comment out original
return text.replace(
_pkgname,
f'#{_pkgname}' + f'_pkgname={converted} #fixed by scancode\n\n'
)
def fix_liburn_prereleases(text, *args):
"""
pkgname=libburn
pkgver=1.5.4
_ver=${pkgver%_p*}
if [ "$_ver" != "$pkgver" ]; then
_pver=".pl${pkgver##*_p}"
fi
"""
return f'_pver="" #fixed by scancode\n\n{text}'
def fix_cmake(text, *args):
"""
case $pkgver in
*.*.*.*) _v=v${pkgver%.*.*};;
*.*.*) _v=v${pkgver%.*};;
esac
"""
version = get_pkgver_substring(text, version_segments=2)
return f'_v=v{version} #fixed by scancode\n\n{text}'
def fix_kamailio(text, *args):
"""
pkgname=kamailio
pkgver=5.4.5
pkgrel=0
# If building from a git snapshot, specify the gitcommit
# If building a proper release, leave gitcommit blank or commented
#_gitcommit=991fe9b28e0e201309048f3b38a135037e40357a
[ -n "$_gitcommit" ] && pkgver="$pkgver.$(date +%Y%m%d)"
[ -n "$_gitcommit" ] && _suffix="-${_gitcommit:0:7}"
[ -n "$_gitcommit" ] && builddir="$srcdir/$pkgname-$_gitcommit" || builddir="$srcdir/$pkgname-$pkgver"
[ -z "$_gitcommit" ] && _gitcommit="$pkgver"
"""
return (
f'_gitcommit="$pkgver" #fixed by scancode\n'
f'_suffix="" #fixed by scancode\n'
f'\n{text}'
)
def fix_qt(text, *args):
"""
case $pkgver in
*_alpha*|*_beta*|*_rc*) _rel=development_releases;;
*) _rel=official_releases;;
esac
"""
pkgver = extract_pkgver(text)
if any(s in pkgver for s in ('_alpha', '_beta', '_rc')):
_rel = 'development_releases'
else:
_rel = 'official_releases'
return (
f'_rel={_rel} #fixed by scancode\n\n'
f'\n{text}'
)
def fix_parole(text, *args):
"""
pkgname=parole
pkgver=1.0.5
pkgrel=0
case $pkgver in
*.*.*.*) _branch=${pkgver%.*.*};;
*.*.*) _branch=${pkgver%.*};;
esac
"""
return add_version_substring_variable(text, varname='_branch', version_segments=2)
def fix_mpd(text, *args):
"""
pkgname=mpd
pkgver=0.22.8
case $pkgver in
*.*.*) _branch=${pkgver%.*};;
*.*) _branch=$pkgver;;
esac
"""
return add_version_substring_variable(text, varname='_branch', version_segments=2)
@attr.s
class ApkBuildFixer:
"""
Represent a syntax fix:
``if_these_strings_are_present`` in ``text``, call ``function(text, *args)``
that returns ``text``.
"""
if_these_strings_are_present = attr.ib(default=tuple())
function = attr.ib(default=replace_fix)
args = attr.ib(default=tuple())
def fix_apkbuild(text):
"""
Return a ``text`` applying some refinements and fixes.
This applies a list of special cases fixes represented by ApkBuildFixer
instances. These are unfortunate hacks to cope with limitations of shell
parameter expansion that would be hard to fix OR to handle parameters that
are not available OR just because it would require executing a build
otherwise.
"""
replacements = [
ApkBuildFixer(
if_these_strings_are_present=('pkgname=qt',),
function=fix_qt,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=gcc\n',),
function=replace_fix,
args=('$_target', ''),
),
ApkBuildFixer(
if_these_strings_are_present=('jool', 'For custom kernels set $FLAVOR.', '_flavor="$FLAVOR"',),
function=replace_fix,
args=('_flavor="$FLAVOR"', '_flavor=lts'),
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=ufw\n',),
function=replace_fix,
args=('$(echo $pkgver|cut -c1-4)', '$pkgver'),
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=rtpengine-$_flavor',),
function=replace_fix,
args=('# rtpengine version\n', '_flavor=lts\n'),
),
ApkBuildFixer(
if_these_strings_are_present=('\t*.*.*.*) _v=${pkgver%.*};;', '\t*.*.*) _v=$pkgver;;'),
function=fix_libreoffice_version,
),
ApkBuildFixer(
if_these_strings_are_present=('\t*.90*) _rel=unstable;;', '\t*) _rel=stable;;'),
function=fix_kde_version,
),
ApkBuildFixer(
if_these_strings_are_present=('_kernver=${pkgver%.*};;', '_kernver=$pkgver;;'),
function=fix_kernver_version,
),
ApkBuildFixer(
if_these_strings_are_present=('_kernver=${pkgver%.*};;', '_kernver=${pkgver};;'),
function=fix_kernver_version,
),
ApkBuildFixer(
if_these_strings_are_present=('\t*.*.*) _v=${pkgver%.*};;', '\t*.*) _v=$pkgver;;'),
function=fix_dunder_v_version,
),
ApkBuildFixer(
if_these_strings_are_present=('_realver=${pkgver%_*}${pkgver#*_}', '_realver=$pkgver'),
function=fix_sudo_realver,
),
ApkBuildFixer(
if_these_strings_are_present=('*.*.*.*) _v=${pkgver%.*.*};;', '*.*.*) _v=${pkgver%.*};;', '*) _v=$pkgver;;'),
function=fix_sqsh_2_segments_version,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=sqlite',),
function=fix_sqlite_version,
),
ApkBuildFixer(
if_these_strings_are_present=("_pkgname=$(echo ${pkgname/-/} | tr '[:lower:]' '[:upper:]')",),
function=fix_lua_pkgname,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=libburn',),
function=fix_liburn_prereleases,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=kamailio',),
function=fix_kamailio,
),
ApkBuildFixer(
if_these_strings_are_present=('_v=v${pkgver%.*.*};;', '_v=v${pkgver%.*};;',),
function=fix_cmake,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=parole',),
function=fix_parole,
),
ApkBuildFixer(
if_these_strings_are_present=('pkgname=mpd',),
function=fix_mpd,
),
]
for fixer in replacements:
if all(s in text for s in fixer.if_these_strings_are_present):
text = fixer.function(text, *fixer.args)
return text
def parse_apkbuild_text(text, strict=False,):
"""
Return an AlpinePackage object from an APKBUILD text context or None. Only
consider variables with a name listed in the ``names`` set.
"""
if not text:
return
variables, errors = get_apkbuild_variables(text=text)
unresolved = [
v for v in variables
if not v.is_resolved()
and v.name in ESSENTIAL_APKBUILD_VARIABLES
]
if strict and unresolved:
raise ApkbuildParseFailure(
f'Failed to parse APKBUILD: {text}\n\n'
f'variables: {variables}\n\n'
f'unresolved: {unresolved}\n\n'
f'errors: {errors}',
)
variables = ((v.name, v.value,) for v in variables)
package = build_package(variables)
if package and unresolved:
unresolved = [v.to_dict() for v in unresolved]
package.extra_data['apkbuild_variable_resolution_errors'] = unresolved
return package
def parse_pkginfo(location):
"""
Return an AlpinePackage object from aa .PKGINFO file at ``location`` or None.
.PKGINFO is a file created by abuild from package metadata in APKBUILD
and that is found at the root of a package .apk tarball.
Each lines are in the format of "name = value" such as in::
# Generated by abuild 3.8.0_rc3-r2
# using fakeroot version 1.25.3
# Wed Jun 9 21:24:56 UTC 2021
pkgname = a2ps
pkgver = 4.14-r9
pkgdesc = a2ps is an Any to PostScript filter
url = https://www.gnu.org/software/a2ps/
builddate = 1623273896
packager = Buildozer <alpine-devel@lists.alpinelinux.org>
size = 3362816
arch = armv7
origin = a2ps
commit = 0a4f8e4e4d21ac8c83a85c534f6424f03a3b7a70
maintainer = Natanael Copa <ncopa@alpinelinux.org>
license = GPL-3.0
depend = ghostscript
depend = imagemagick
"""
raise NotImplementedError('TODO: implement me')
def build_package(package_fields):
"""
Return an AlpinePackage object from a `package_fields` iterable of (name,
value) tuples. The package_fields comes from the APKINDEX and installed
database files that use one-letter field names.
Note: we do NOT use a dict for ``package_fields`` because some fields names
may occur more than once.
See for details:
https://wiki.alpinelinux.org/wiki/Apk_spec#Install_DB
https://wiki.alpinelinux.org/wiki/APKBUILD_Reference
https://wiki.alpinelinux.org/wiki/Alpine_package_format
https://git.alpinelinux.org/apk-tools/tree/src/package.c?id=82de29cf7bad3d9cbb0aeb4dbe756ad2bde73eb3#n774
"""
package_fields = list(package_fields)
all_fields = dict(package_fields)
# mapping of actual Package field name -> value that have been converted to
# the expected normalized format
converted_fields = {}
for name, value in package_fields:
handler = package_handlers_by_field_name.get(name)
if handler:
try:
converted = handler(value, all_fields=all_fields, **converted_fields)
except:
raise Exception(*list(package_fields))
# for extra data we update the existing
extra_data = converted.pop('extra_data', {}) or {}
if extra_data:
existing_extra_data = converted_fields.get('extra_data')
if existing_extra_data:
existing_extra_data.update(extra_data)
else:
converted_fields['extra_data'] = dict(extra_data)
converted_fields.update(converted)
return AlpinePackage.create(**converted_fields)
# Note handlers MUST accept **kwargs as they also receive the current data
# being processed so far as a processed_data kwarg, but most do not use it
def build_name_value_str_handler(name):
"""
Return a generic handler callable function for plain string fields with the
name ``name``.
"""
def handler(value, **kwargs):
return {name: value}
return handler
def apkbuild_version_handler(value, all_fields, **kwargs):
pkgrel = all_fields.get('pkgrel')
rel_suffix = f'-r{pkgrel}' if pkgrel else ''
return {'version': f'{value}{rel_suffix}'}
def L_license_handler(value, **kwargs):
"""
Return a normalized declared license and a detected license expression.
"""
original = value
_declared, detected = detect_declared_license(value)
return {
'declared_license': original,
'license_expression': detected,
}
def S_size_handler(value, **kwargs):
return {'size': int(value)}
def t_release_date_handler(value, **kwargs):
"""
Return a Package data mapping for a buiddate timestamp.
"""
value = int(value)
dt = datetime.utcfromtimestamp(value)
stamp = dt.isoformat()
# we get 2020-01-15T10:36:22, but care only for the date part
date, _, _time = stamp.partition('T')
return {'release_date': date}
get_maintainers = re.compile(
r'(?P<name>[^<]+)'
r'\s?'
r'(?P<email><[^>]+>)'
).findall
def m_maintainer_handler(value, **kwargs):
"""
Return a Package data mapping as a list of parties a maintainer Party.
A maintainer value may be one or more mail name <email@example.com> parts, space-separated.
"""
parties = []
for name, email in get_maintainers(value):
maintainer = models.Party(
type='person',
role='maintainer',
name=name,
email=email,
)
parties.append(maintainer)
return {'parties': parties}
# this will return a three tuple if there is a split or a single item otherwise
split_name_and_requirement = re.compile('(=~|>~|<~|~=|~>|~<|=>|>=|=<|<=|<|>|=)').split
def D_dependencies_handler(value, dependencies=None, **kwargs):
"""
Return a list of dependent packages from a dependency string and from previous dependencies.
Dependencies can be either:
- a package name with or without a version constraint
- a path to something that is provided by some package(similar to RPMs) such as /bin/sh
- a shared object (prefixed with so:)
- a pkgconfig dependency where pkgconfig is typically also part of the deps
and these are prefixed with pc:
- a command prefixed with cmd:
Note that these exist the same way in the p: provides field.
An exclamation prefix negates the dependency.
The operators can be ><=~
For example:
D:aaudit a52dec=0.7.4-r7 acf-alpine-baselayout>=0.5.7 /bin/sh
D:so:libc.musl-x86_64.so.1 so:libmilter.so.1.0.2
D:freeradius>3
D:lua5.1-bit32<26
D:!bison so:libc.musl-x86_64.so.1
D:python3 pc:atk>=2.15.1 pc:cairo pc:xkbcommon>=0.2.0 pkgconfig
D:bash colordiff cmd:ssh cmd:ssh-keygen cmd:ssh-keyscan cmd:column
D:cmd:nginx nginx-mod-devel-kit so:libc.musl-aarch64.so.1
D:mu=1.4.12-r0 cmd:emacs
"""
# operate on a copy for safety and create an empty list on first use
dependencies = dependencies[:] if dependencies else []
for dep in value.split():
if dep.startswith('!'):
# ignore the "negative" deps, we cannot do much with it
# they are more of a hints for the dep solver than something
# actionable for origin reporting
continue
if dep.startswith('/'):
# ignore paths to a command for now as we cannot do
# much with them yet until we can have a Package URL for them.
continue
if dep.startswith('cmd:'):
# ignore commands for now as we cannot do much with them yet until
# we can have a Package URL for them.
continue
if dep.startswith('so:'):
# ignore the shared object with an so: prexi for now as we cannot do
# much with them yet until we can have a Package URL for them.
# TODO: see how we could handle these and similar used elsewhere
continue
is_pc = False
if dep.startswith('pc:'):
is_pc = True
# we strip the 'pc:' prefix and treat a pc: dependency the same as
# other depends
dep = dep[3:]
requirement = None
version = None
is_resolved = False
segments = split_name_and_requirement(dep)
if len(segments) == 1:
# we have no requirement...just a plain name
name = dep
else:
if len(segments) != 3:
raise Exception(dependencies, kwargs)
name, operator, ver = segments
# normalize operator tsuch that >= and => become =>
operator = ''.join(sorted(operator))
if operator == '=':
version = ver
is_resolved = True
requirement = operator + ver
purl = PackageURL(type='alpine', name=name, version=version).to_string()
# that the only scope we have for now
scope = 'depend'
if is_pc:
scope += ':pkgconfig'
dependency = models.DependentPackage(
purl=purl,
scope=scope,
requirement=requirement,
is_resolved=is_resolved,
)
if dependency not in dependencies:
dependencies.append(dependency)
return {'dependencies': dependencies}
def o_source_package_handler(value, version=None, **kwargs):
"""
Return a source_packages list of Package URLs
"""
# the version value will be that of the current package
origin = PackageURL(type='alpine', name=value, version=version).to_string()
return {'source_packages': [origin]}
def c_git_commit_handler(value, **kwargs):
"""
Return a git VCS URL from a package commit.
"""
return {f'vcs_url': f'git+http://git.alpinelinux.org/aports/commit/?id={value}'}
def A_arch_handler(value, **kwargs):
"""
Return a Package URL qualifier for the arch.
"""
return {'qualifiers': f'arch={value}'}
# Note that we use a little trick for handling files.
# Each handler receives a copy of the data processed so far.
# As it happens, the data about files start with a directory enry
# then one or more files, each normally followed by their checksums
# We return and use the current_dir and current_file from these handlers
# to properly create a file for its directory (which is the current one)
# and add the checksum to its file (which is the current one).
# 'current_file' and 'current_dir' are not actual package fields, but we ignore
# these when we create the AlpinePcakge object
def F_directory_handler(value, **kwargs):
return {'current_dir': value}
def R_filename_handler(value, current_dir, installed_files=None, **kwargs):
"""
Return a new current_file PackageFile in current_dir. Add to installed_files
"""
# operate on a copy for safety and create an empty list on first use
installed_files = installed_files[:] if installed_files else []
current_file = models.PackageFile(path=posixpath.join(current_dir, value))
installed_files.append(current_file)
return {'current_file': current_file, 'installed_files': installed_files}
def Z_checksum_handler(value, current_file, **kwargs):
"""
Update the current PackageFile with its updated SHA1 hex-encoded checksum.
'Z' is a file checksum (for files and links)
For example: Z:Q1WTc55xfvPogzA0YUV24D0Ym+MKE=
The 1st char is an encoding code: Q means base64 and the 2nd char is
the type of checksum: 1 means SHA1 so Q1 means based64-encoded SHA1
"""
assert value.startswith('Q1'), (
f'Unknown checksum or encoding: should start with Q1 for base64-encoded SHA1: {value}')
sha1 = base64.b64decode(value[2:])
sha1 = codecs.encode(sha1, 'hex').decode('utf-8').lower()
current_file.sha1 = sha1
return {'current_file': current_file}
def get_checksum_entries(value):
"""
Yield tuples of (file_name, checksum) for each checksum found in
an APKBUILD checksums ``value`` variable.
See https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#md5sums.2Fsha256sums.2Fsha512sums
> Checksums for the files/URLs listed in source. The checksums are
> normally generated and updated by executing abuild checksum and should
> be the last item in the APKBUILD.
The shape of this is one entry per line and two spaces separator with filenames::
sha512sums="db2e3cf88d8d18 a52dec-0.7.4.tar.gz
db2e3cf88d8d12 automake.patch
21d44824109ea6 fix-globals-test-x86-pie.patch
29e7269873806e a52dec-0.7.4-build.patch" "
"""
for entry in value.strip().splitlines(False):
entry = entry.strip()
if not entry:
continue
if not ' ' in entry:
raise Exception(f'Invalid APKBUILD checksums format: {value!r}')
checksum, _, file_name = entry.partition(' ')
checksum = checksum.strip()
file_name = file_name.strip()
yield file_name, checksum
def checksums_handler(value, checksum_name, **kwargs):
"""