Gentoo Archives: gentoo-portage-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-portage-dev@l.g.o
Cc: "Michał Górny" <mgorny@g.o>
Subject: [gentoo-portage-dev] [PATCH 4/6] Add EAPI 7 version comparison & manipulation functions
Date: Tue, 06 Mar 2018 16:55:34
Message-Id: 20180306165505.19087-5-mgorny@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH 0/6] EAPI 7, part II by "Michał Górny"
1 Bug: https://bugs.gentoo.org/482170
2 ---
3 bin/eapi.sh | 4 +
4 bin/eapi7-ver-funcs.sh | 191 ++++++++++++++++++++
5 bin/phase-helpers.sh | 4 +
6 pym/portage/tests/bin/test_eapi7_ver_funcs.py | 240 ++++++++++++++++++++++++++
7 4 files changed, 439 insertions(+)
8 create mode 100644 bin/eapi7-ver-funcs.sh
9 create mode 100644 pym/portage/tests/bin/test_eapi7_ver_funcs.py
10
11 diff --git a/bin/eapi.sh b/bin/eapi.sh
12 index 5d77c8daf..cd2acad80 100644
13 --- a/bin/eapi.sh
14 +++ b/bin/eapi.sh
15 @@ -108,6 +108,10 @@ ___eapi_has_sandbox_rm_functions() {
16 [[ ! ${1-${EAPI-0}} =~ ^(0|1|2|3|4|4-python|4-slot-abi|5|5-progress|6)$ ]]
17 }
18
19 +___eapi_has_version_functions() {
20 + [[ ! ${1-${EAPI-0}} =~ ^(0|1|2|3|4|4-python|4-slot-abi|5|5-progress|6)$ ]]
21 +}
22 +
23 ___eapi_has_master_repositories() {
24 [[ ${1-${EAPI-0}} =~ ^(5-progress)$ ]]
25 }
26 diff --git a/bin/eapi7-ver-funcs.sh b/bin/eapi7-ver-funcs.sh
27 new file mode 100644
28 index 000000000..b4e98f4e7
29 --- /dev/null
30 +++ b/bin/eapi7-ver-funcs.sh
31 @@ -0,0 +1,191 @@
32 +#!/bin/bash
33 +# Copyright 1999-2018 Gentoo Foundation
34 +# Distributed under the terms of the GNU General Public License v2
35 +
36 +__eapi7_ver_parse_range() {
37 + local range=${1}
38 + local max=${2}
39 +
40 + [[ ${range} == [0-9]* ]] \
41 + || die "${FUNCNAME}: range must start with a number"
42 + start=${range%-*}
43 + [[ ${range} == *-* ]] && end=${range#*-} || end=${start}
44 + if [[ ${end} ]]; then
45 + [[ ${start} -le ${end} ]] \
46 + || die "${FUNCNAME}: end of range must be >= start"
47 + [[ ${end} -le ${max} ]] || end=${max}
48 + else
49 + end=${max}
50 + fi
51 +}
52 +
53 +__eapi7_ver_split() {
54 + local v=${1} LC_ALL=C
55 +
56 + comp=()
57 +
58 + # get separators and components
59 + local s c
60 + while [[ ${v} ]]; do
61 + # cut the separator
62 + s=${v%%[a-zA-Z0-9]*}
63 + v=${v:${#s}}
64 + # cut the next component; it can be either digits or letters
65 + [[ ${v} == [0-9]* ]] && c=${v%%[^0-9]*} || c=${v%%[^a-zA-Z]*}
66 + v=${v:${#c}}
67 +
68 + comp+=( "${s}" "${c}" )
69 + done
70 +}
71 +
72 +ver_cut() {
73 + local range=${1}
74 + local v=${2:-${PV}}
75 + local start end
76 + local -a comp
77 +
78 + __eapi7_ver_split "${v}"
79 + local max=$((${#comp[@]}/2))
80 + __eapi7_ver_parse_range "${range}" "${max}"
81 +
82 + local IFS=
83 + if [[ ${start} -gt 0 ]]; then
84 + start=$(( start*2 - 1 ))
85 + fi
86 + echo "${comp[*]:start:end*2-start}"
87 +}
88 +
89 +ver_rs() {
90 + local v
91 + (( ${#} & 1 )) && v=${@: -1} || v=${PV}
92 + local start end i
93 + local -a comp
94 +
95 + __eapi7_ver_split "${v}"
96 + local max=$((${#comp[@]}/2 - 1))
97 +
98 + while [[ ${#} -ge 2 ]]; do
99 + __eapi7_ver_parse_range "${1}" "${max}"
100 + for (( i = start*2; i <= end*2; i+=2 )); do
101 + [[ ${i} -eq 0 && -z ${comp[i]} ]] && continue
102 + comp[i]=${2}
103 + done
104 + shift 2
105 + done
106 +
107 + local IFS=
108 + echo "${comp[*]}"
109 +}
110 +
111 +__eapi7_ver_compare_int() {
112 + local a=$1 b=$2 d=$(( ${#1}-${#2} ))
113 +
114 + # Zero-pad to equal length if necessary.
115 + if [[ ${d} -gt 0 ]]; then
116 + printf -v b "%0${d}d%s" 0 "${b}"
117 + elif [[ ${d} -lt 0 ]]; then
118 + printf -v a "%0$(( -d ))d%s" 0 "${a}"
119 + fi
120 +
121 + [[ ${a} > ${b} ]] && return 3
122 + [[ ${a} == "${b}" ]]
123 +}
124 +
125 +__eapi7_ver_compare() {
126 + local va=${1} vb=${2} a an al as ar b bn bl bs br re LC_ALL=C
127 +
128 + re="^([0-9]+(\.[0-9]+)*)([a-z]?)((_(alpha|beta|pre|rc|p)[0-9]*)*)(-r[0-9]+)?$"
129 +
130 + [[ ${va} =~ ${re} ]] || die "${FUNCNAME}: invalid version: ${va}"
131 + an=${BASH_REMATCH[1]}
132 + al=${BASH_REMATCH[3]}
133 + as=${BASH_REMATCH[4]}
134 + ar=${BASH_REMATCH[7]}
135 +
136 + [[ ${vb} =~ ${re} ]] || die "${FUNCNAME}: invalid version: ${vb}"
137 + bn=${BASH_REMATCH[1]}
138 + bl=${BASH_REMATCH[3]}
139 + bs=${BASH_REMATCH[4]}
140 + br=${BASH_REMATCH[7]}
141 +
142 + # Compare numeric components (PMS algorithm 3.2)
143 + # First component
144 + __eapi7_ver_compare_int "${an%%.*}" "${bn%%.*}" || return
145 +
146 + while [[ ${an} == *.* && ${bn} == *.* ]]; do
147 + # Other components (PMS algorithm 3.3)
148 + an=${an#*.}
149 + bn=${bn#*.}
150 + a=${an%%.*}
151 + b=${bn%%.*}
152 + if [[ ${a} == 0* || ${b} == 0* ]]; then
153 + # Remove any trailing zeros
154 + [[ ${a} =~ 0+$ ]] && a=${a%"${BASH_REMATCH[0]}"}
155 + [[ ${b} =~ 0+$ ]] && b=${b%"${BASH_REMATCH[0]}"}
156 + [[ ${a} > ${b} ]] && return 3
157 + [[ ${a} < ${b} ]] && return 1
158 + else
159 + __eapi7_ver_compare_int "${a}" "${b}" || return
160 + fi
161 + done
162 + [[ ${an} == *.* ]] && return 3
163 + [[ ${bn} == *.* ]] && return 1
164 +
165 + # Compare letter components (PMS algorithm 3.4)
166 + [[ ${al} > ${bl} ]] && return 3
167 + [[ ${al} < ${bl} ]] && return 1
168 +
169 + # Compare suffixes (PMS algorithm 3.5)
170 + as=${as#_}${as:+_}
171 + bs=${bs#_}${bs:+_}
172 + while [[ -n ${as} && -n ${bs} ]]; do
173 + # Compare each suffix (PMS algorithm 3.6)
174 + a=${as%%_*}
175 + b=${bs%%_*}
176 + if [[ ${a%%[0-9]*} == "${b%%[0-9]*}" ]]; then
177 + __eapi7_ver_compare_int "${a##*[a-z]}" "${b##*[a-z]}" || return
178 + else
179 + # Check for p first
180 + [[ ${a%%[0-9]*} == p ]] && return 3
181 + [[ ${b%%[0-9]*} == p ]] && return 1
182 + # Hack: Use that alpha < beta < pre < rc alphabetically
183 + [[ ${a} > ${b} ]] && return 3 || return 1
184 + fi
185 + as=${as#*_}
186 + bs=${bs#*_}
187 + done
188 + if [[ -n ${as} ]]; then
189 + [[ ${as} == p[_0-9]* ]] && return 3 || return 1
190 + elif [[ -n ${bs} ]]; then
191 + [[ ${bs} == p[_0-9]* ]] && return 1 || return 3
192 + fi
193 +
194 + # Compare revision components (PMS algorithm 3.7)
195 + __eapi7_ver_compare_int "${ar#-r}" "${br#-r}" || return
196 +
197 + return 2
198 +}
199 +
200 +ver_test() {
201 + local va op vb
202 +
203 + if [[ $# -eq 3 ]]; then
204 + va=${1}
205 + shift
206 + else
207 + va=${PVR}
208 + fi
209 +
210 + [[ $# -eq 2 ]] || die "${FUNCNAME}: bad number of arguments"
211 +
212 + op=${1}
213 + vb=${2}
214 +
215 + case ${op} in
216 + -eq|-ne|-lt|-le|-gt|-ge) ;;
217 + *) die "${FUNCNAME}: invalid operator: ${op}" ;;
218 + esac
219 +
220 + __eapi7_ver_compare "${va}" "${vb}"
221 + test $? "${op}" 2
222 +}
223 diff --git a/bin/phase-helpers.sh b/bin/phase-helpers.sh
224 index 3a2138636..8bb110082 100644
225 --- a/bin/phase-helpers.sh
226 +++ b/bin/phase-helpers.sh
227 @@ -1172,6 +1172,10 @@ if ___eapi_has_in_iuse; then
228 }
229 fi
230
231 +if ___eapi_has_version_functions; then
232 + source "${PORTAGE_BIN_PATH}/eapi7-ver-funcs.sh" || exit 1
233 +fi
234 +
235 if ___eapi_has_master_repositories; then
236 master_repositories() {
237 local output repository=$1 retval
238 diff --git a/pym/portage/tests/bin/test_eapi7_ver_funcs.py b/pym/portage/tests/bin/test_eapi7_ver_funcs.py
239 new file mode 100644
240 index 000000000..408975298
241 --- /dev/null
242 +++ b/pym/portage/tests/bin/test_eapi7_ver_funcs.py
243 @@ -0,0 +1,240 @@
244 +# Copyright 2018 Gentoo Foundation
245 +# Distributed under the terms of the GNU General Public License v2
246 +
247 +import subprocess
248 +import tempfile
249 +
250 +from portage.const import PORTAGE_BIN_PATH
251 +from portage.tests import TestCase
252 +
253 +
254 +class TestEAPI7VerFuncs(TestCase):
255 + def _test_output(self, test_cases):
256 + """
257 + Test that commands in test_cases produce expected output.
258 + """
259 + with tempfile.NamedTemporaryFile('w') as test_script:
260 + test_script.write('source "%s"/eapi7-ver-funcs.sh\n'
261 + % (PORTAGE_BIN_PATH,))
262 + for cmd, exp in test_cases:
263 + test_script.write('%s\n' % (cmd,))
264 + test_script.flush()
265 +
266 + s = subprocess.Popen(['bash', test_script.name],
267 + stdout=subprocess.PIPE,
268 + stderr=subprocess.PIPE)
269 + sout, serr = s.communicate()
270 + self.assertEqual(s.returncode, 0)
271 +
272 + for test_case, result in zip(test_cases, sout.decode().splitlines()):
273 + cmd, exp = test_case
274 + self.assertEqual(result, exp,
275 + '%s -> %s; expected: %s' % (cmd, result, exp))
276 +
277 + def _test_return(self, test_cases):
278 + """
279 + Test that commands in test_cases give appropriate exit codes.
280 + """
281 + with tempfile.NamedTemporaryFile('w+') as test_script:
282 + test_script.write('source "%s"/eapi7-ver-funcs.sh\n'
283 + % (PORTAGE_BIN_PATH,))
284 + for cmd, exp in test_cases:
285 + test_script.write('%s; echo $?\n' % (cmd,))
286 + test_script.flush()
287 +
288 + s = subprocess.Popen(['bash', test_script.name],
289 + stdout=subprocess.PIPE,
290 + stderr=subprocess.PIPE)
291 + sout, serr = s.communicate()
292 + self.assertEqual(s.returncode, 0)
293 +
294 + for test_case, result in zip(test_cases, sout.decode().splitlines()):
295 + cmd, exp = test_case
296 + self.assertEqual(result, exp,
297 + '%s -> %s; expected: %s' % (cmd, result, exp))
298 +
299 + def _test_fail(self, test_cases):
300 + """
301 + Test that commands in test_cases fail.
302 + """
303 +
304 + for cmd in test_cases:
305 + test = '''
306 +source "%s"/eapi7-ver-funcs.sh
307 +die() { exit 1; }
308 +%s''' % (PORTAGE_BIN_PATH, cmd)
309 +
310 + s = subprocess.Popen(['bash', '-c', test],
311 + stdout=subprocess.PIPE,
312 + stderr=subprocess.PIPE)
313 + sout, serr = s.communicate()
314 + self.assertEqual(s.returncode, 1,
315 + '"%s" did not fail; output: %s; %s)'
316 + % (cmd, sout.decode(), serr.decode()))
317 +
318 + def test_ver_cut(self):
319 + test_cases = [
320 + # (command, output)
321 + ('ver_cut 1 1.2.3', '1'),
322 + ('ver_cut 1-1 1.2.3', '1'),
323 + ('ver_cut 1-2 1.2.3', '1.2'),
324 + ('ver_cut 2- 1.2.3', '2.3'),
325 + ('ver_cut 1- 1.2.3', '1.2.3'),
326 + ('ver_cut 3-4 1.2.3b_alpha4', '3b'),
327 + ('ver_cut 5 1.2.3b_alpha4', 'alpha'),
328 + ('ver_cut 1-2 .1.2.3', '1.2'),
329 + ('ver_cut 0-2 .1.2.3', '.1.2'),
330 + ('ver_cut 2-3 1.2.3.', '2.3'),
331 + ('ver_cut 2- 1.2.3.', '2.3.'),
332 + ('ver_cut 2-4 1.2.3.', '2.3.'),
333 + ]
334 + self._test_output(test_cases)
335 +
336 + def test_ver_rs(self):
337 + test_cases = [
338 + # (command, output)
339 + ('ver_rs 1 - 1.2.3', '1-2.3'),
340 + ('ver_rs 2 - 1.2.3', '1.2-3'),
341 + ('ver_rs 1-2 - 1.2.3.4', '1-2-3.4'),
342 + ('ver_rs 2- - 1.2.3.4', '1.2-3-4'),
343 + ('ver_rs 2 . 1.2-3', '1.2.3'),
344 + ('ver_rs 3 . 1.2.3a', '1.2.3.a'),
345 + ('ver_rs 2-3 - 1.2_alpha4', '1.2-alpha-4'),
346 + ('ver_rs 3 - 2 "" 1.2.3b_alpha4', '1.23-b_alpha4'),
347 + ('ver_rs 3-5 _ 4-6 - a1b2c3d4e5', 'a1b_2-c-3-d4e5'),
348 + ('ver_rs 1 - .1.2.3', '.1-2.3'),
349 + ('ver_rs 0 - .1.2.3', '-1.2.3'),
350 + ]
351 + self._test_output(test_cases)
352 +
353 + def test_truncated_range(self):
354 + test_cases = [
355 + # (command, output)
356 + ('ver_cut 0-2 1.2.3', '1.2'),
357 + ('ver_cut 2-5 1.2.3', '2.3'),
358 + ('ver_cut 4 1.2.3', ''),
359 + ('ver_cut 0 1.2.3', ''),
360 + ('ver_cut 4- 1.2.3', ''),
361 + ('ver_rs 0 - 1.2.3', '1.2.3'),
362 + ('ver_rs 3 . 1.2.3', '1.2.3'),
363 + ('ver_rs 3- . 1.2.3', '1.2.3'),
364 + ('ver_rs 3-5 . 1.2.3', '1.2.3'),
365 + ]
366 + self._test_output(test_cases)
367 +
368 + def test_invalid_range(self):
369 + test_cases = [
370 + 'ver_cut foo 1.2.3',
371 + 'ver_rs -3 _ a1b2c3d4e5',
372 + 'ver_rs 5-3 _ a1b2c3d4e5',
373 + ]
374 + self._test_fail(test_cases)
375 +
376 + def test_ver_test(self):
377 + test_cases = [
378 + # Tests from Portage's test_vercmp.py
379 + ('ver_test 6.0 -gt 5.0', '0'),
380 + ('ver_test 5.0 -gt 5', '0'),
381 + ('ver_test 1.0-r1 -gt 1.0-r0', '0'),
382 + ('ver_test 999999999999999999 -gt 999999999999999998', '0'), # 18 digits
383 + ('ver_test 1.0.0 -gt 1.0', '0'),
384 + ('ver_test 1.0.0 -gt 1.0b', '0'),
385 + ('ver_test 1b -gt 1', '0'),
386 + ('ver_test 1b_p1 -gt 1_p1', '0'),
387 + ('ver_test 1.1b -gt 1.1', '0'),
388 + ('ver_test 12.2.5 -gt 12.2b', '0'),
389 + ('ver_test 4.0 -lt 5.0', '0'),
390 + ('ver_test 5 -lt 5.0', '0'),
391 + ('ver_test 1.0_pre2 -lt 1.0_p2', '0'),
392 + ('ver_test 1.0_alpha2 -lt 1.0_p2', '0'),
393 + ('ver_test 1.0_alpha1 -lt 1.0_beta1', '0'),
394 + ('ver_test 1.0_beta3 -lt 1.0_rc3', '0'),
395 + ('ver_test 1.001000000000000001 -lt 1.001000000000000002', '0'),
396 + ('ver_test 1.00100000000 -lt 1.001000000000000001', '0'),
397 + ('ver_test 999999999999999998 -lt 999999999999999999', '0'),
398 + ('ver_test 1.01 -lt 1.1', '0'),
399 + ('ver_test 1.0-r0 -lt 1.0-r1', '0'),
400 + ('ver_test 1.0 -lt 1.0-r1', '0'),
401 + ('ver_test 1.0 -lt 1.0.0', '0'),
402 + ('ver_test 1.0b -lt 1.0.0', '0'),
403 + ('ver_test 1_p1 -lt 1b_p1', '0'),
404 + ('ver_test 1 -lt 1b', '0'),
405 + ('ver_test 1.1 -lt 1.1b', '0'),
406 + ('ver_test 12.2b -lt 12.2.5', '0'),
407 + ('ver_test 4.0 -eq 4.0', '0'),
408 + ('ver_test 1.0 -eq 1.0', '0'),
409 + ('ver_test 1.0-r0 -eq 1.0', '0'),
410 + ('ver_test 1.0 -eq 1.0-r0', '0'),
411 + ('ver_test 1.0-r0 -eq 1.0-r0', '0'),
412 + ('ver_test 1.0-r1 -eq 1.0-r1', '0'),
413 + ('ver_test 1 -eq 2', '1'),
414 + ('ver_test 1.0_alpha -eq 1.0_pre', '1'),
415 + ('ver_test 1.0_beta -eq 1.0_alpha', '1'),
416 + ('ver_test 1 -eq 0.0', '1'),
417 + ('ver_test 1.0-r0 -eq 1.0-r1', '1'),
418 + ('ver_test 1.0-r1 -eq 1.0-r0', '1'),
419 + ('ver_test 1.0 -eq 1.0-r1', '1'),
420 + ('ver_test 1.0-r1 -eq 1.0', '1'),
421 + ('ver_test 1.0 -eq 1.0.0', '1'),
422 + ('ver_test 1_p1 -eq 1b_p1', '1'),
423 + ('ver_test 1b -eq 1', '1'),
424 + ('ver_test 1.1b -eq 1.1', '1'),
425 + ('ver_test 12.2b -eq 12.2', '1'),
426 +
427 + # A subset of tests from Paludis
428 + ('ver_test 1.0_alpha -gt 1_alpha', '0'),
429 + ('ver_test 1.0_alpha -gt 1', '0'),
430 + ('ver_test 1.0_alpha -lt 1.0', '0'),
431 + ('ver_test 1.2.0.0_alpha7-r4 -gt 1.2_alpha7-r4', '0'),
432 + ('ver_test 0001 -eq 1', '0'),
433 + ('ver_test 01 -eq 001', '0'),
434 + ('ver_test 0001.1 -eq 1.1', '0'),
435 + ('ver_test 01.01 -eq 1.01', '0'),
436 + ('ver_test 1.010 -eq 1.01', '0'),
437 + ('ver_test 1.00 -eq 1.0', '0'),
438 + ('ver_test 1.0100 -eq 1.010', '0'),
439 + ('ver_test 1-r00 -eq 1-r0', '0'),
440 +
441 + # Additional tests
442 + ('ver_test 0_rc99 -lt 0', '0'),
443 + ('ver_test 011 -eq 11', '0'),
444 + ('ver_test 019 -eq 19', '0'),
445 + ('ver_test 1.2 -eq 001.2', '0'),
446 + ('ver_test 1.2 -gt 1.02', '0'),
447 + ('ver_test 1.2a -lt 1.2b', '0'),
448 + ('ver_test 1.2_pre1 -gt 1.2_pre1_beta2', '0'),
449 + ('ver_test 1.2_pre1 -lt 1.2_pre1_p2', '0'),
450 + ('ver_test 1.00 -lt 1.0.0', '0'),
451 + ('ver_test 1.010 -eq 1.01', '0'),
452 + ('ver_test 1.01 -lt 1.1', '0'),
453 + ('ver_test 1.2_pre08-r09 -eq 1.2_pre8-r9', '0'),
454 + ('ver_test 0 -lt 576460752303423488', '0'), # 2**59
455 + ('ver_test 0 -lt 9223372036854775808', '0'), # 2**63
456 + ]
457 + self._test_return(test_cases)
458 +
459 + def test_invalid_test(self):
460 + test_cases = [
461 + # Bad number or ordering of arguments
462 + 'ver_test 1',
463 + 'ver_test 1 -lt 2 3',
464 + 'ver_test -lt 1 2',
465 +
466 + # Bad operators
467 + 'ver_test 1 "<" 2',
468 + 'ver_test 1 lt 2',
469 + 'ver_test 1 -foo 2',
470 +
471 + # Malformed versions
472 + 'ver_test "" -ne 1',
473 + 'ver_test 1. -ne 1',
474 + 'ver_test 1ab -ne 1',
475 + 'ver_test b -ne 1',
476 + 'ver_test 1-r1_pre -ne 1',
477 + 'ver_test 1-pre1 -ne 1',
478 + 'ver_test 1_foo -ne 1',
479 + 'ver_test 1_pre1.1 -ne 1',
480 + 'ver_test 1-r1.0 -ne 1',
481 + 'ver_test cvs.9999 -ne 9999',
482 + ]
483 + self._test_fail(test_cases)
484 --
485 2.16.2