Gentoo Archives: gentoo-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-dev@l.g.o, konsolebox <konsolebox@×××××.com>
Subject: Re: [gentoo-dev] JFYIOR: A Simple Package Versioning Spec
Date: Fri, 18 Sep 2015 18:24:02
Message-Id: 53F3B63E-A9A1-4892-BAFD-CBCA7BB78D18@gentoo.org
In Reply to: [gentoo-dev] JFYIOR: A Simple Package Versioning Spec by konsolebox
1 Dnia 18 września 2015 11:32:15 CEST, konsolebox <konsolebox@×××××.com> napisał(a):
2 >This is what an ideal and simple versioning spec should look like to
3 >me. (Not the form, but the concept). I'm posting this here so it
4 >could be used as an added reference to anyone that would consider
5 >revising the current specification.
6
7 The length of this mail itself proves that this is far from simple. And similarly to the current solution it's full of silly special cases and magical rules. If you really want something simple and clean, take a look at the scheme used by pkg-config and rpm.
8
9 >
10 >Note: Assigning default values can be bypassed depending on the
11 >implementation.
12 >
13 >Comments are welcome.
14 >----------------------------------------
15 >1 Version Specification
16 >
17 >A version is composed of four component sets: the base part, the
18 >stage, the patch and the revision.
19 >
20 >Each component is composed mainly of version nodes to represent their
21 >level values.
22 >
23 >When a component is not specified, it gets a default node value of {0}.
24 >
25 >1.1 Version Nodes
26 >
27 >Nodes start basically with a number and then is optionally followed by
28 >a set of letters.
29 >
30 >Numbers and letters can coexist alternatingly to represent a single
31 >node. The number of consecutive letters is also not limited to 1.
32 >For example: "1a4xy" is allowed (can be restricted).
33 >
34 >Each set of digits represents a single decimal number. Leading zeros
35 >hold no meaning.
36 >
37 >The numerical equivalent of a set of letters is calculated in base of
38 >27 (0 exists along with it but is not included as a symbol).
39 >
40 >Version nodes after processing are basically just an array of signed
41 >integers where each set of digits or letters are converted to their
42 >numerical value.
43 >
44 >1.2 Version Parts
45 >
46 >1.2.1 The Base Part
47 >
48 >The base version is simply a set of version nodes that are separated
49 >by dots. Examples: "4.1.2", "4.1.2aa" "4a", "4a.1", "4a.1.2".
50 >
51 >After processing, the base version is an array of version nodes.
52 >
53 >It is required.
54 >
55 >1.2.2 The Stage Part
56 >
57 >The stage part starts with _alpha, _beta, _pre or _rc. Their
58 >numerical values are -4, -3, -2 or -1 respectively. Each of them can
59 >optionally be followed by a version node string. For example:
60 >"_alpha01".
61 >
62 >The resulting value for the stage after processing is a single version
63 >node where the first value in it is the numerical values of _alpha,
64 >_beta, _pre or _rc, and the other values are based on the added node
65 >string.
66 >
67 >A version without a stage has a default stage value of {0}.
68 >
69 >The stage part is optional and can be specified only once after the
70 >base part. It can't be specified as a modifier for the patch, for the
71 >revision, or for another stage.
72 >
73 >1.2.3 The Patch Part
74 >
75 >The patch part begins with _p and is followed by a version node
76 >string. For example: "_p20150105a".
77 >
78 >The patch part is optional and can be specified after the base part or
79 >after the stage part, but not after the revision. It can only be
80 >specified once.
81 >
82 >It is processed as a single version node based on its version node
83 >string.
84 >
85 >A version without a patch has a default patch value of {0}.
86 >
87 >1.2.4 The Revision
88 >
89 >The revision starts with -r and is followed by a number.
90 >
91 >It is processed as a single version node based on its version node
92 >string.
93 >
94 >A version without a revision has a default revision value of {0}.
95 >
96 >1.3 Comparing Versions
97 >
98 >Versions are compared as version nodes and the algorithm is simple:
99 >each component and subcomponent is compared from left to right.
100 >
101 >Anything that gives a difference decides which version is greater or
102 >lesser.
103 >
104 >Any non-existing version-node has a default value of {0}.
105 >
106 >Any non-existing element of a version node has a default value of 0.
107 >
108 >If no difference is found during the process, it would mean that both
109 >versions are equal.
110 >
111 >1.3.1 Concept Code
112 >
113 > #!/usr/bin/ruby
114 >
115 > class ::Array
116 > def adaptive_transpose
117 > h_size = self.max_by{ |a| a.size }.size
118 > v_size = self.size
119 >
120 > result = Array.new(h_size)
121 > with_index = self.each_with_index.to_a.freeze
122 >
123 > 0.upto(h_size - 1) do |i|
124 > result[i] = Array.new(v_size)
125 > with_index.each{ |a, j| result[i][j] = a[i] }
126 > end
127 >
128 > result
129 > end
130 > end
131 >
132 > module Portage
133 > class PackageVersion
134 > class Node < ::Array
135 > def initialize(*values)
136 > self.concat(values)
137 > end
138 >
139 > def compare_with(another)
140 > [self, another].adaptive_transpose.each do |a, b|
141 > a ||= 0
142 > b ||= 0
143 > return -1 if a < b
144 > return 1 if a > b
145 > end
146 >
147 > return 0
148 > end
149 >
150 > def self.parse(*args)
151 > result = new
152 >
153 > args.each do |a|
154 > case a
155 > when Integer
156 > result << a
157 > when /^[[:digit:]][[:alnum:]]*$/
158 > a.scan(/[[:digit:]]+|[[:alpha:]]+/).each_with_index.map do |b, i|
159 > if i.even?
160 > str = b.gsub(/^0+/, '')
161 > result << (str.empty? ? 0 : Integer(str))
162 > else
163 > value = 0
164 >
165 > b.downcase.bytes.reverse.each_with_index do |c, i|
166 > value += 27 ** i * (c - 96) ## a == 1, z == 26,
167 >and 0 exists but is not used
168 > end
169 >
170 > result << value
171 > end
172 > end
173 > else
174 > raise ArgumentError.new("Invalid node string: #{a.inspect}")
175 > end
176 > end
177 >
178 > result
179 > end
180 >
181 > def self.zero
182 > @zero ||= new(0)
183 > end
184 >
185 > private_class_method :new, :allocate
186 > end
187 >
188 > attr_accessor :base, :stage, :patch, :revision
189 >
190 > def initialize(base, stage, patch, revision)
191 > @base, @stage, @patch, @revision = base, stage, patch, revision
192 > end
193 >
194 > def compare_with(another)
195 > [self.base, another.base].adaptive_transpose.each do |a, b|
196 > a ||= Node.zero
197 > b ||= Node.zero
198 > r = a.compare_with(b)
199 > return r unless r == 0
200 > end
201 >
202 > r = self.stage.compare_with(another.stage)
203 > return r unless r == 0
204 >
205 > r = self.patch.compare_with(another.patch)
206 > return r unless r == 0
207 >
208 > r = self.revision.compare_with(another.revision)
209 > return r unless r == 0
210 >
211 > return 0
212 > end
213 >
214 > STAGES = { 'alpha' => -4, 'beta' => -3, 'pre' => -2, 'rc' => -1 }
215 >REGEX =
216 >/^([[:digit:]][[:alnum:]]*(?:[.][[:alnum:]]+)*)?(?:_(alpha|beta|pre|rc)([[:digit:]][[:alnum:]]*)?)?(?:_p([[:digit:]][[:alnum:]]*))?(?:-r([[:digit:]]+))?(.+)?$/m
217 >
218 > def self.parse(version_string)
219 > __, base, stage, stage_ver, patch, revision, extra =
220 >version_string.match(REGEX).to_a
221 > raise_invalid_version_string(version_string) if extra
222 >
223 > begin
224 > base = base.split('.').map{ |e| Node.parse(e) }
225 > stage = stage ? stage_ver ? Node.parse(STAGES[stage],
226 >stage_ver) : Node.parse(STAGES[stage]) : Node.zero
227 > patch = patch ? Node.parse(patch) : Node.zero
228 > revision = revision ? Node.parse(revision) : Node.zero
229 > rescue ArgumentError => e
230 > raise_invalid_version_string("#{version_string}: #{e}")
231 > end
232 >
233 > new(base, stage, patch, revision)
234 > end
235 >
236 > def self.raise_invalid_version_string(version_string)
237 > raise ArgumentError.new("Invalid version string: #{version_string}")
238 > end
239 >
240 > private_class_method :new, :allocate, :raise_invalid_version_string
241 > end
242 > end
243 >
244 > samples = [
245 > ["0", "0.01"],
246 > ["0.01", "0.010"],
247 > ["0.09", "0.090"],
248 > ["0.10", "0.100"],
249 > ["0.99", "0.990"],
250 > ["0.100", "0.1000"],
251 > ["0.100", "0.100"],
252 > ["0.1", "0.1.1"],
253 > ["0.1.1", "0.1a"],
254 > ["0.1a", "0.2"],
255 > ["0.2", "1"],
256 > ["1", "1.0"],
257 > ["1.0", "1.0_alpha"],
258 > ["1.0_alpha", "1.0_alpha01"],
259 > ["1.0_alpha01", "1.0_alpha01-r1"],
260 > ["1.0_alpha01-r1", "1.0_alpha01_p20150105"],
261 > ["1.0_alpha01_p20150105", "1.0_alpha01_p20150105-r1"],
262 > ["1.0_alpha01", "1.0_beta"],
263 > ["1.0_beta", "1.0_beta01"],
264 > ["1.0_beta01", "1.0_pre01"],
265 > ["1.0_pre01", "1.0_rc01"],
266 > ["1.0_rc01", "1.0"],
267 > ["1.0", "1.0-r1"],
268 > ["1.0-r1", "1.0_p20150105"],
269 > ["1.0_p20150105", "1.0_p20150105-r1"]
270 > ]
271 >
272 > samples.each do |a, b|
273 > x = Portage::PackageVersion.parse(a)
274 > y = Portage::PackageVersion.parse(b)
275 > r = x.compare_with(y)
276 > r = r < 0 ? '<' : r > 0 ? '>' : '=='
277 > puts "#{a} #{r} #{b}"
278 > end
279 >
280 >1.3.2 Concept Code Output
281 >
282 > 0 < 0.01
283 > 0.01 < 0.010
284 > 0.09 < 0.090
285 > 0.10 < 0.100
286 > 0.99 < 0.990
287 > 0.100 < 0.1000
288 > 0.100 == 0.100
289 > 0.1 < 0.1.1
290 > 0.1.1 < 0.1a
291 > 0.1a < 0.2
292 > 0.2 < 1
293 > 1 == 1.0
294 > 1.0 > 1.0_alpha
295 > 1.0_alpha < 1.0_alpha01
296 > 1.0_alpha01 < 1.0_alpha01-r1
297 > 1.0_alpha01-r1 < 1.0_alpha01_p20150105
298 > 1.0_alpha01_p20150105 < 1.0_alpha01_p20150105-r1
299 > 1.0_alpha01 < 1.0_beta
300 > 1.0_beta < 1.0_beta01
301 > 1.0_beta01 < 1.0_pre01
302 > 1.0_pre01 < 1.0_rc01
303 > 1.0_rc01 < 1.0
304 > 1.0 < 1.0-r1
305 > 1.0-r1 < 1.0_p20150105
306 > 1.0_p20150105 < 1.0_p20150105-r1
307
308 --
309 Best regards,
310 Michał Górny