aur.rb 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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