aur.rb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,599 @@
1
+ require 'tsort'
2
+ require 'time'
3
+
4
+ module Archlinux
5
+
6
+ class PackageClass #meta class for class that hold package infos
7
+ def self.packages(*repos)
8
+ pkgs=PackageList.new
9
+ repos.each do |repo|
10
+ npkg=case repo
11
+ when "@db"
12
+ Archlinux.config.db.packages
13
+ when "@dbdir"
14
+ Archlinux.config.db.dir_packages
15
+ when ":local"
16
+ LocalRepo.new.packages
17
+ when /^@get/, /^@rget/
18
+ new=Archlinux.config.install_list
19
+ m=repo.match(/^@r?get\((.*)\)/)
20
+ l=m[1].split(',')
21
+ #require 'pry'; binding.pry
22
+ if repo =~ /^@rget/
23
+ new.rget(*l)
24
+ else
25
+ new.get(*l)
26
+ end
27
+ new
28
+ else
29
+ if (m=repo.match(/^:(.*)\Z/))
30
+ Repo.new(m[1]).packages
31
+ else
32
+ path=Pathname.new(repo)
33
+ if path.file?
34
+ PackageFiles.new(path).packages
35
+ elsif path.directory?
36
+ PackageFiles.from_dir(path).packages
37
+ end
38
+ end
39
+ end
40
+ if npkg.nil?
41
+ SH.logger.warn "Unknown repo #{repo}"
42
+ else
43
+ pkgs.merge(npkg)
44
+ end
45
+ end
46
+ pkgs
47
+ end
48
+
49
+ def self.packages_list(*repos)
50
+ if repos.length == 2
51
+ pkg1=packages(*repos[0])
52
+ pkg2=packages(*repos[1])
53
+ else
54
+ i=repos.index('::')
55
+ unless i
56
+ SH.logger.warn "Only one list given"
57
+ i=0
58
+ end
59
+ pkg1=packages(*repos[0...i])
60
+ pkg2=packages(*repos[i+1..-1])
61
+ end
62
+ return pkg1, pkg2
63
+ end
64
+ end
65
+ PackageError=Class.new(ArchlinuxError)
66
+ class Package
67
+ def self.create(v)
68
+ v.is_a?(self) ? v : self.new(v)
69
+ end
70
+
71
+ Archlinux.delegate_h(self, :@props)
72
+ attr_reader :name, :props
73
+
74
+ def initialize(*args)
75
+ case args.length
76
+ when 2
77
+ name, props=args
78
+ when 1
79
+ props=args.first
80
+ name=nil
81
+ else
82
+ raise PackageError.new("Error the number of arguments should be 1 or 2")
83
+ end
84
+ @name=name
85
+ @props={}
86
+ self.props=(props)
87
+ @name=@props[:name] || @props[:pkgname] || @props[:pkgbase] unless @name
88
+ end
89
+
90
+ def props=(props)
91
+ [:groups, :depends, :make_depends, :check_depends, :conflicts, :replaces, :provides, :depends_for, :opt_depends_for].each do |k|
92
+ props.key?(k) or @props[k]=[]
93
+ end
94
+ @props[:opt_depends]||={}
95
+ props.each do |k,v|
96
+ k=Utils.to_snake_case(k.to_s).to_sym
97
+ k=:opt_depends if k==:optdepends or k==:optdepend or k==:optional_deps
98
+ k=:make_depends if k==:makedepends or k==:makedepend
99
+ k=:check_depends if k==:checkdepends or k==:checkdepend
100
+ k=:build_date if k==:builddate
101
+ k=:depends if k==:depends_on or k==:requires
102
+ k=:conflicts if k==:conflicts_with
103
+ k=:pkgbase if k==:base
104
+ k=:depends_for if k==:required_by
105
+ k=:opt_depends_for if k==:optional_for
106
+ k=:description if k==:desc or k==:pkgdesc
107
+ case k
108
+ when :first_submitted, :last_modified, :out_of_date, :build_date, :install_date
109
+ if v and !v.is_a?(Time)
110
+ v= v.is_a?(Integer) ? Time.at(v) : Time.parse(v)
111
+ end
112
+ when :repository, :groups, :depends, :make_depends, :check_depends, :conflicts, :replaces, :provides, :depends_for, :opt_depends_for, :license, :source
113
+ v=Array(v)
114
+ when :opt_depends
115
+ unless v.is_a?(Hash)
116
+ w={}
117
+ Array(v).each do |l|
118
+ l.match(/(\w*)\s+:\s+(.*)/) do |m|
119
+ w[m[1]]=m[2]
120
+ end
121
+ end
122
+ v=w
123
+ end
124
+ end
125
+ @props[k]=v
126
+ end
127
+ if !@props[:version] and @props[:pkgver]
128
+ @props[:version]=Version.new(@props[:epoch], @props[:pkgver], @props[:pkgrel]).to_s
129
+ end
130
+ end
131
+
132
+ def merge(h)
133
+ h.each do |k,v|
134
+ if @props[k].nil? or ((k==:package_size or k==:download_size or k==:installed_size) and (@props[k]=="0.00 B" or @props[k]==0))
135
+ @props[k]=v
136
+ elsif k==:repository
137
+ @props[k]=(Array(@props[k])+v).uniq
138
+ end
139
+ end
140
+ self
141
+ end
142
+
143
+ def dependencies(l=%i(depends))
144
+ l.map {|i| a=@props[i]; a.is_a?(Hash) ? a.keys : Array(a)}.flatten.uniq
145
+ end
146
+
147
+ def version
148
+ Version.new(@props[:version])
149
+ end
150
+
151
+ def name_version
152
+ r=self.name
153
+ return r if r.nil?
154
+ version=self.version
155
+ r+="="+version.to_s if version
156
+ r
157
+ end
158
+
159
+ def file
160
+ @props[:filename] && Pathname.new(@props[:filename])
161
+ end
162
+
163
+ def path
164
+ file || Pathname.new(@props[:repo])
165
+ end
166
+
167
+ def same?(other)
168
+ # @props.slice(*(@props.keys - [:repo])) == other.props.slice(*(other.props.keys - [:repo]))
169
+ slice=%i(version description depends provides opt_depends replaces conflicts)
170
+ # p name, other.name, @props.slice(*slice), other.props.slice(*slice)
171
+ name == other.name && @props.slice(*slice) == other.props.slice(*slice)
172
+ end
173
+ end
174
+
175
+ class PackageList
176
+ extend CreateHelper
177
+
178
+ Archlinux.delegate_h(self, :@l)
179
+ attr_accessor :children_mode, :ext_query, :ignore, :query_ignore, :install_list, :install_method, :install_list_of
180
+ attr_reader :l, :versions, :provides_for
181
+
182
+ def initialize(list=[], config: Archlinux.config)
183
+ @l={}
184
+ @versions={} #hash of versions for quick look ups
185
+ @provides_for={} #hash of provides for quick look ups
186
+ @ext_query=nil #how to get missing packages, default
187
+ @children_mode=%i(depends) #children, default
188
+ @ignore=[] #ignore packages update
189
+ @query_ignore=[] #query ignore packages (ie these won't be returned by a query; so stronger than @ignore)
190
+ @install_list=nil #how do we check for new packages / updates
191
+ @install_list_of=nil #are we used to check for new packages / updates
192
+ @install_method=nil #how to install stuff
193
+ @config=config
194
+ merge(list)
195
+ end
196
+
197
+ # the names without the versions. Use self.keys to get the name=version
198
+ def names
199
+ @versions.keys
200
+ end
201
+
202
+ def list(version=false)
203
+ l= version ? keys.sort : names.sort
204
+ l.each do |pkg|
205
+ SH.logger.info "- #{pkg}"
206
+ end
207
+ end
208
+
209
+ def name_of(pkg)
210
+ pkg.name_version
211
+ end
212
+
213
+ def packages
214
+ self
215
+ end
216
+
217
+ def same?(other)
218
+ unless @l.keys == other.keys
219
+ SH.logger.warn("#{self.class}: Inconsistency in the package names")
220
+ return false
221
+ end
222
+ r=true
223
+ @l.each do |name, pkg|
224
+ unless pkg.same?(other[name])
225
+ SH.logger.warn("#{self.class}: Inconsistensy for the package #{name}")
226
+ r=false
227
+ end
228
+ end
229
+ return r
230
+ end
231
+
232
+ def merge(l)
233
+ l= case l
234
+ when PackageList
235
+ l.values
236
+ when Hash
237
+ l.values.compact
238
+ else
239
+ l.to_a
240
+ end
241
+ l.each do |pkg|
242
+ pkg=Package.new(pkg) unless pkg.is_a?(Package)
243
+ name=name_of(pkg)
244
+ if @l.key?(name)
245
+ @l[name].merge(pkg)
246
+ else
247
+ @l[name]=pkg
248
+ end
249
+
250
+ @versions[pkg.name]||={}
251
+ @versions[pkg.name][pkg.version.to_s]=name
252
+
253
+ pkg[:provides].each do |p|
254
+ pkg=Query.strip(p)
255
+ @provides_for[pkg]||={}
256
+ @provides_for[pkg][p]=name #todo: do we pass the name or the full pkg?
257
+ #todo: we need a list here
258
+ end
259
+ end
260
+ self
261
+ end
262
+
263
+ # return all packages that provides for pkg
264
+ # this is more complicated than @provides_for[pkg]
265
+ # because if b provides a and c provides a, then pacman assumes that b
266
+ # provides c
267
+ def all_provides_for(pkg)
268
+ provides=l.fetch(pkg,{}).fetch(:provides,[]).map {|q| Query.strip(q)}
269
+ provided=([Query.strip(pkg)]+provides).flat_map do |prov|
270
+ @provides_for.fetch(prov,{}).values.map {|v| Version.strip(v)}
271
+ end.uniq
272
+ provided
273
+ end
274
+
275
+ def latest
276
+ r={}
277
+ @versions.each do |pkg, versions|
278
+ v=versions.keys.max do |v1,v2|
279
+ Version.create(v1) <=> Version.create(v2)
280
+ end
281
+ r[pkg]=@l[versions[v]]
282
+ end
283
+ r
284
+ end
285
+
286
+ # select the most appropriate match (does not use ext_query), using #query
287
+ def find(q, **opts)
288
+ return q if @l.key?(q)
289
+ q=Query.create(q); pkg=q.name
290
+ query(q, **opts) do |found, type|
291
+ if type==:version
292
+ unless found.empty? #we select the most up to date
293
+ max = found.max { |v,w| Version.create(v) <=> Version.create(w) }
294
+ return @versions[pkg][max]
295
+ end
296
+ elsif type==:provides
297
+ max = found.max { |v,w| Query.create(v) <=> Query.create(w) }
298
+ return @provides_for[pkg][max]
299
+ end
300
+ end
301
+ return nil
302
+ end
303
+
304
+ # output all matches (does not use ext_query)
305
+ def query(q, provides: false) #provides: do we check Provides?
306
+ q=Query.new(q) unless q.is_a?(Query)
307
+ matches=[]; pkg=q.name
308
+ if @versions.key?(pkg)
309
+ matches+=(found=@versions[pkg].keys.select {|v| q.satisfy?(Version.create(v))}).map {|k| @versions[pkg][k]}
310
+ yield(found, :version) if block_given?
311
+ end
312
+ if provides and @provides_for.key?(pkg)
313
+ matches+=(found=@provides_for[pkg].keys.select {|v| q.satisfy?(Query.create(v))}).map {|k| @provides_for[pkg][k]}
314
+ yield(found, :provides) if block_given?
315
+ end
316
+ matches
317
+ end
318
+
319
+ # here the arguments are Strings
320
+ # return the arguments replaced by eventual provides + missing packages
321
+ # are added to @l
322
+ # So this is like find, except we respect @query_ignore, and call
323
+ # ext_query for missing packages
324
+ def resolve(*queries, provides: true, ext_query: @ext_query, fallback: true, log_missing: :warn, log_fallback: :warn, **opts)
325
+ got={}; missed=[]
326
+ pkgs=queries.map {|p| Query.strip(p)}
327
+ ignored = pkgs & @query_ignore
328
+ queries.each do |query|
329
+ if ignored.include?(Query.strip(query))
330
+ got[query]=nil #=> means the query was ignored
331
+ else
332
+ pkg=self.find(query, provides: provides, **opts)
333
+ if pkg
334
+ got[query]=pkg
335
+ else
336
+ missed << query
337
+ end
338
+ end
339
+ end
340
+ # we do it this way to call ext_query in batch
341
+ if ext_query and !missed.empty?
342
+ found, new_pkgs=ext_query.call(*missed, provides: provides)
343
+ self.merge(new_pkgs)
344
+ got.merge!(found)
345
+ missed-=found.keys
346
+ end
347
+ if fallback and !missed.empty?
348
+ new_queries={}
349
+ missed.each do |query|
350
+ if (query_pkg=Query.strip(query)) != query
351
+ new_queries[query]=query_pkg
352
+ # missed.delete(query)
353
+ end
354
+ end
355
+ unless new_queries.empty?
356
+ SH.log(log_fallback, "Trying fallback for packages: #{new_queries.keys.join(', ')}")
357
+ fallback_got=self.resolve(*new_queries.values, provides: provides, ext_query: ext_query, fallback: false, log_missing: :verbose, **opts)
358
+ got.merge!(fallback_got)
359
+ SH.log(log_missing, "#{self.class}: Warning! Missing packages: #{missed.map {|m| r=m; r<<" [fallback: #{fallback}]" if (fallback=fallback_got[new_queries[m]]); r}.join(', ')}") unless missed.empty?
360
+ end
361
+ else
362
+ SH.log(log_missing, "#{self.class}: Warning! Missing packages: #{missed.join(', ')}") unless missed.empty?
363
+ end
364
+ got
365
+ end
366
+
367
+ # this gives the keys of the packages we resolved
368
+ def get(*args)
369
+ #compact because the resolution can be nil for an ignored package
370
+ resolve(*args).values.compact
371
+ end
372
+
373
+ # this gives the values of the packages we resolved
374
+ def get_packages(*args)
375
+ l.values_at(*get(*args))
376
+ end
377
+
378
+ def get_package(pkg)
379
+ l[(get(pkg).first)]
380
+ end
381
+
382
+ # this is like a 'restrict' operation
383
+ def slice(*args)
384
+ self.class.new(l.slice(*get(*args)), **{})
385
+ end
386
+
387
+ # get children (non recursive)
388
+ def children(node, mode=@children_mode, log_level: :verbose2, **opts, &b)
389
+ deps=@l.fetch(node).dependencies(mode)
390
+ SH.log(log_level, "- #{node}: #{deps.join(', ')}")
391
+ deps=get(*deps, **opts)
392
+ SH.log(log_level, " => #{deps.join(', ')}") unless deps.empty?
393
+ if b
394
+ deps.each(&b)
395
+ else
396
+ deps
397
+ end
398
+ end
399
+
400
+ private def call_tsort(l, method: :tsort, **opts, &b)
401
+ each_node=l.method(:each)
402
+ s=self
403
+ each_child = lambda do |node, &b|
404
+ s.children(node, **opts, &b)
405
+ end
406
+ TSort.public_send(method, each_node, each_child, &b)
407
+ end
408
+
409
+ def tsort(l, **opts, &b)
410
+ if b
411
+ call_tsort(l, method: :each_strongly_connected_component, **opts, &b)
412
+ else
413
+ r=call_tsort(l, method: :strongly_connected_components, **opts)
414
+ cycles=r.select {|c| c.length > 1}
415
+ SH.logger.warn "Cycles detected: #{cycles}" unless cycles.empty?
416
+ r.flatten
417
+ end
418
+ end
419
+
420
+ # recursive get
421
+ def rget(*pkgs)
422
+ l=get(*pkgs)
423
+ tsort(l)
424
+ end
425
+
426
+ # check updates compared to another list
427
+ def check_updates(l, ignore: @ignore)
428
+ l=self.class.create(l)
429
+ a=self.latest; b=l.latest
430
+ r={}
431
+ b.each do |k, v|
432
+ next if ignore.include?(k)
433
+ if a.key?(k)
434
+ v1=a[k].version
435
+ v2=v.version
436
+ h={in: v1.to_s, out: v2.to_s, in_pkg: name_of(a[k]), out_pkg: name_of(v)}
437
+ case v1 <=> v2
438
+ when -1
439
+ h[:op]=:upgrade
440
+ when 0
441
+ h[:op]=:equal
442
+ when 1
443
+ h[:op]=:downgrade
444
+ end
445
+ r[k]=h
446
+ else
447
+ #new installation
448
+ r[k]={op: :install,
449
+ in: nil,
450
+ out: v.version.to_s, out_pkg: name_of(v)}
451
+ end
452
+ end
453
+ (a.keys-b.keys).each do |k|
454
+ next if ignore.include?(k)
455
+ r[k]={op: :obsolete,
456
+ in: a[k].version.to_s,
457
+ out: nil, in_pkg: name_of(a[k])}
458
+ end
459
+ r
460
+ end
461
+
462
+ def select_updates(r)
463
+ up=r.select {|_k,v| v[:op]==:upgrade or v[:op]==:install}
464
+ return up.map {|_k, v| v[:out_pkg]}, up
465
+ end
466
+
467
+ def get_updates(l, log_level: true, ignore: @ignore, rebuild: false, **showopts)
468
+ c=check_updates(l, ignore: ignore)
469
+ show_updates(c, log_level: log_level, **showopts)
470
+ if rebuild
471
+ # keep all packages
472
+ to_build=c.select {|_k,v| v[:out_pkg]}
473
+ return to_build.map {|_k, v| v[:out_pkg]}, to_build
474
+ else
475
+ select_updates(c)
476
+ end
477
+ end
478
+
479
+ #take the result of check_updates and pretty print them
480
+ #no_show has priority over :show
481
+ def show_updates(r, show: [:upgrade, :downgrade, :obsolete, :install], no_show: [], log_level: true)
482
+ r.each do |k,v|
483
+ next unless show.include?(v[:op]) and !no_show.include?(v[:op])
484
+ vin= v[:in] ? v[:in] : "(none)"
485
+ vout= v[:out] ? v[:out] : "(none)"
486
+ op=case v[:op]
487
+ when :downgrade
488
+ "<-"
489
+ when :upgrade, :install, :obsolete
490
+ "->"
491
+ when :equal
492
+ "="
493
+ end
494
+ extra=""
495
+ extra=" [#{v[:op]}]" if v[:op]!=:upgrade
496
+
497
+ SH.log(log_level, " -> #{k}: #{vin} #{op} #{vout}#{extra}")
498
+ end
499
+ end
500
+
501
+ # this take a list of packages which can be updates of ours
502
+ # return check_updates of this list (restricted to our current
503
+ # packages, so it won't show any 'install' operation
504
+ def check_update(updates=@install_list, ignore: @ignore)
505
+ return [] if updates.nil?
506
+ new_pkgs=updates.slice(*names) #ignore 'install' packages
507
+ check_updates(new_pkgs, ignore: ignore)
508
+ end
509
+
510
+ def update?(**opts)
511
+ install?(update: true, **opts)
512
+ end
513
+
514
+ # take a list of packages to install, return the new or updated
515
+ # packages to install with their dependencies
516
+ def install?(*packages, update: false, install_list: @install_list, log_level: true, log_level_verbose: :verbose, ignore: @ignore, rebuild: false, no_show: [:obsolete], **showopts)
517
+ packages+=self.names if update
518
+ if install_list
519
+ ignore -= packages.map {|p| Query.strip(p)}
520
+ SH.log(log_level_verbose, "# Checking packages #{packages.join(', ')}", color: :bold)
521
+ new_pkgs=install_list.slice(*packages)
522
+ u, u_infos=get_updates(new_pkgs, log_level: log_level_verbose, ignore: ignore, rebuild: rebuild, no_show: no_show, **showopts)
523
+ # todo: update this when we have a better preference mechanism
524
+ # (then we will need to put @official in the install package class)
525
+ new=self.class.new(l.values).merge(new_pkgs)
526
+ new.chain_query(install_list)
527
+ # The updates or new packages may need new deps
528
+ SH.log(log_level_verbose, "# Checking dependencies of #{u.join(', ')}", color: :bold) unless u.empty?
529
+ full=new.rget(*u)
530
+ SH.log(log_level, "New packages:", color: :bold)
531
+ full_updates, full_infos=get_updates(new.slice(*full), log_level: log_level, ignore: ignore, rebuild: rebuild=="full" ? true : false, no_show: no_show, **showopts)
532
+ if rebuild and rebuild != "full" #we need to merge back u
533
+ full_updates |=u
534
+ full_infos.merge!(u_infos)
535
+ end
536
+ infos={top_pkgs: u_infos, all_pkgs: full_infos}
537
+ full_updates, infos=yield full_updates, infos if block_given?
538
+ return full_updates, infos
539
+ else
540
+ SH.logger.warn "External install list not defined"
541
+ end
542
+ end
543
+
544
+ def update(**opts, &b)
545
+ install(update: true, **opts, &b)
546
+ end
547
+
548
+ # the callback is passed to install? while the block is passed to
549
+ # @install_method
550
+ def install(*args, callback: nil, **opts, &b)
551
+ install_opts={}
552
+ keys=method(:install?).parameters.select {|arg| arg[0]==:key}.map {|arg| arg[1]}
553
+ keys.each do |key|
554
+ case key
555
+ when :rebuild
556
+ opts.key?(key) && install_opts[key]=opts.fetch(key)
557
+ else
558
+ opts.key?(key) && install_opts[key]=opts.delete(key)
559
+ end
560
+ end
561
+ l, l_info=install?(*args, **install_opts, &callback) #return false in the callback to prevent install
562
+ if @install_method
563
+ @install_method.call(l, pkgs_info: l_info, **opts, &b) unless !l or l.empty?
564
+ else
565
+ return l, l_info
566
+ end
567
+ end
568
+
569
+ #returns a Proc that can be used for another PackageList as an ext_query
570
+ def to_ext_query
571
+ method(:as_ext_query)
572
+ end
573
+
574
+ def chain_query(ext_query)
575
+ ext_query=ext_query.to_ext_query if ext_query.is_a?(PackageList)
576
+ if @ext_query
577
+ orig_query=@ext_query
578
+ @ext_query = lambda do |*args, **opts|
579
+ r, l=orig_query.call(*args, **opts)
580
+ missed = args-r.keys
581
+ r2, l2=ext_query.call(*missed, **opts)
582
+ return r.merge(r2), l.merge(l2)
583
+ end
584
+ else
585
+ @ext_query=ext_query
586
+ end
587
+ end
588
+
589
+ # essentially just a wrapper around resolve
590
+ def as_ext_query(*queries, provides: false, full_pkgs: false)
591
+ r=self.resolve(*queries, provides: provides, fallback: false)
592
+ # puts "#{self.class}: #{queries} => #{r}"
593
+ l= full_pkgs ? @l : slice(*r.values.compact)
594
+ return r, l
595
+ end
596
+
597
+ end
598
+
599
+ end