Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/portupgrade
Path: blob/master/lib/pkgtools/portsdb.rb
102 views
1
# vim: set sts=2 sw=2 ts=8 et:
2
#
3
# Copyright (c) 2001-2004 Akinori MUSHA <[email protected]>
4
# Copyright (c) 2006-2008 Sergey Matveychuk <[email protected]>
5
# Copyright (c) 2009-2012 Stanislav Sedov <[email protected]>
6
# Copyright (c) 2012 Bryan Drewery <[email protected]>
7
#
8
# All rights reserved.
9
#
10
# Redistribution and use in source and binary forms, with or without
11
# modification, are permitted provided that the following conditions
12
# are met:
13
# 1. Redistributions of source code must retain the above copyright
14
# notice, this list of conditions and the following disclaimer.
15
# 2. Redistributions in binary form must reproduce the above copyright
16
# notice, this list of conditions and the following disclaimer in the
17
# documentation and/or other materials provided with the distribution.
18
#
19
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29
# SUCH DAMAGE.
30
#
31
32
require 'singleton'
33
require 'tempfile'
34
require 'pkgtools/pkgmisc'
35
require 'pkgtools/pkgdbtools'
36
37
class PortsDB
38
include Singleton
39
include Enumerable
40
include PkgDBTools
41
42
DB_VERSION = [:FreeBSD, 4]
43
44
LANGUAGE_SPECIFIC_CATEGORIES = {
45
"arabic" => "ar-",
46
"chinese" => "zh-",
47
"french" => "fr-",
48
"german" => "de-",
49
"hebrew" => "iw-",
50
"hungarian" => "hu-",
51
"japanese" => "ja-",
52
"korean" => "ko-",
53
"polish" => "pl-",
54
"portuguese" => "pt-",
55
"russian" => "ru-",
56
"ukrainian" => "uk-",
57
"vietnamese" => "vi-",
58
}
59
60
MY_PORT = 'ports-mgmt/portupgrade'
61
62
LOCK_FILE = '/var/run/portsdb.lock'
63
64
attr_accessor :ignore_categories, :extra_categories, :moved
65
66
class IndexFileError < StandardError
67
# def message
68
# "index file error"
69
# end
70
end
71
72
class IndexFileFetchError< IndexFileError
73
# def message
74
# "index file fetch error"
75
# end
76
end
77
78
class DBError < StandardError
79
# def message
80
# "database file error"
81
# end
82
end
83
84
class MOVEDError < StandardError
85
# def message
86
# "MOVED file error"
87
# end
88
end
89
90
class MovedElement
91
attr_reader :to, :date, :why, :seq
92
def initialize(to, date, why, seq)
93
@to = to
94
@date = date
95
@why = why
96
@seq = seq
97
end
98
end
99
100
class Moved
101
MOVED_FILE = 'MOVED'
102
103
def initialize(ports_dir)
104
@moved = Hash.new
105
@seq = 0
106
fill(File.join(ports_dir, MOVED_FILE))
107
end
108
109
def fill(moved_file)
110
if File.exist?(moved_file)
111
File.open(moved_file) do |f|
112
f.each do |line|
113
next if /^[#[:space:]]/ =~ line
114
115
moved_from, moved_to, date, why = line.chomp.split('|')
116
117
if moved_from.nil? || moved_to.nil? || date.nil? || why.nil?
118
raise MOVEDError, "MOVED file format error"
119
end
120
121
moved_to.empty? and moved_to = nil
122
123
@moved[moved_from] = MovedElement.new(moved_to, date, why, @seq)
124
@seq += 1
125
end
126
end
127
end
128
end
129
130
def trace(port)
131
t = []
132
me = port
133
134
while true
135
if moved = @moved[me]
136
t << moved if t.empty? or t.last.seq < moved.seq
137
if me.nil? or t.map{|p| p.to}.include?(me)
138
break
139
else
140
me = moved.to
141
end
142
else
143
break
144
end
145
end
146
147
if t.empty?
148
nil
149
else
150
t
151
end
152
end
153
end
154
155
def PortsDB.finalizer
156
Proc.new {
157
PkgDBTools.remove_lock(LOCK_FILE)
158
}
159
end
160
161
def setup(alt_db_dir = nil, alt_ports_dir = nil, alt_db_driver = nil)
162
@db = nil
163
@lock_file = Process.euid == 0 ? LOCK_FILE : nil
164
@db_version = DB_VERSION
165
ObjectSpace.define_finalizer(self, PortsDB.finalizer)
166
set_ports_dir(alt_ports_dir)
167
set_db_dir(alt_db_dir)
168
set_db_driver(alt_db_driver)
169
170
@categories = nil
171
@virtual_categories = nil
172
@ignore_categories = []
173
@extra_categories = []
174
@origins = nil
175
@pkgnames = nil
176
@origins_by_categories = {}
177
@ports = {}
178
@localbase = nil
179
@x11base = nil
180
@pkg_sufx = nil
181
@moved = Moved.new(ports_dir)
182
183
self
184
end
185
186
def make_var(var, dir = ports_dir())
187
all_vars = var.is_a?(Array) ? var : [var]
188
vars = all_vars.join(' -V ')
189
file = dir == ports_dir() ? "-f Mk/bsd.port.mk" : ""
190
results = `cd #{dir} && make #{file} -V #{vars}`.lines.map { |val|
191
val.strip!
192
if val.empty?
193
nil
194
else
195
val
196
end
197
}
198
# Return array if requested, or single value if not
199
var.is_a?(Array) ? results : results.first
200
end
201
202
def ports_dir()
203
unless @ports_dir
204
set_ports_dir(nil) # initialize with the default value
205
end
206
207
@ports_dir
208
end
209
210
def ports_dir=(new_ports_dir)
211
@abs_ports_dir = @index_file = @dist_dir = nil
212
@alt_index_files = Array.new
213
214
@ports_dir = new_ports_dir || ENV['PORTSDIR'] || '/usr/ports'
215
end
216
alias set_ports_dir ports_dir=
217
218
def abs_ports_dir()
219
unless @abs_ports_dir
220
dir = ports_dir
221
222
begin
223
Dir.chdir(dir) {
224
@abs_ports_dir = Dir.pwd
225
}
226
rescue => e
227
raise DBError, "Can't chdir to '#{dir}': #{e.message}"
228
end
229
end
230
231
@abs_ports_dir
232
end
233
234
def index_file()
235
unless @index_file
236
indexdir, indexfile = make_var(['INDEXDIR', 'INDEXFILE'])
237
@index_file = ENV['PORTS_INDEX'] || File.join(indexdir, indexfile || 'INDEX')
238
@alt_index_files = config_value(:ALT_INDEX) || []
239
end
240
241
@index_file
242
end
243
244
def db_dir=(new_db_dir)
245
@db_dir = new_db_dir || ENV['PORTS_DBDIR'] || ports_dir
246
247
@db_filebase = File.join(@db_dir, File.basename(index_file()))
248
@db_file = @db_filebase + '.db'
249
250
close_db
251
252
@db_dir
253
end
254
alias set_db_dir db_dir=
255
256
def db_dir_list()
257
[
258
db_dir,
259
ports_dir,
260
PkgDB.instance.db_dir,
261
ENV['TMPDIR'],
262
'/var/tmp',
263
'/tmp'
264
].compact
265
end
266
267
def my_port
268
MY_PORT
269
end
270
271
def my_portdir
272
portdir(MY_PORT)
273
end
274
275
def localbase
276
@localbase ||= make_var('LOCALBASE', my_portdir) || '/usr/local'
277
end
278
279
def x11base
280
@x11base ||= make_var('X11BASE', my_portdir) || '/usr/X11R6'
281
end
282
283
def pkg_sufx
284
@pkg_sufx ||= pkg_sufx!
285
end
286
287
def pkg_sufx!
288
make_var('PKG_SUFX', my_portdir) || ENV['PKG_SUFX'] || '.tbz'
289
end
290
291
def dist_dir()
292
@dist_dir ||= make_var('DISTDIR') || portdir('distfiles')
293
end
294
295
def join(category, port)
296
File.join(category, port)
297
end
298
299
def split(origin)
300
if %r"^([^./A-Z][^/]*)/([^./][^/]*)$" =~ path
301
return $1, $2
302
end
303
304
nil
305
end
306
307
def strip(path, existing_only = false)
308
# handle sequences of /'s (tr_s is not multibyte-aware, hence gsub)
309
path = path.gsub(%r"//+", '/')
310
311
%r"^(?:(/.+)/)?([^./][^/]*/[^./][^/]*)/?$" =~ path or return nil
312
313
dir = $1
314
port = $2
315
316
if dir && dir != ports_dir && dir != abs_ports_dir
317
return nil
318
end
319
320
if existing_only && !exist?(port)
321
return nil
322
end
323
324
port
325
end
326
327
def portdir(port)
328
File.join(ports_dir, port)
329
end
330
331
def subdirs(dir)
332
%x"fgrep SUBDIR #{dir}/Makefile | sed -e 's/SUBDIR +=//'
333
2> /dev/null".split.select { |i|
334
File.directory?(File.join(dir, i))
335
}.sort
336
end
337
338
def categories
339
open_db if @categories.nil?
340
341
@categories
342
end
343
344
def real_categories!
345
subdirs(ports_dir)
346
end
347
348
def categories!
349
customize_categories(real_categories!)
350
end
351
352
def customize_categories(cats)
353
((cats | @extra_categories) - @ignore_categories).sort
354
end
355
356
def category?(category)
357
@categories.qinclude?(category)
358
end
359
360
def virtual_categories
361
open_db if @virtual_categories.nil?
362
363
@virtual_categories
364
end
365
366
def virtual_category?(category)
367
@virtual_categories.qinclude?(category)
368
end
369
370
def ignore_category?(category)
371
@ignore_categories.qinclude?(category)
372
end
373
374
def update(fetch = false)
375
if fetch
376
STDERR.print "Fetching the ports index ... "
377
else
378
STDERR.print "Updating the ports index ... "
379
end
380
381
STDERR.flush
382
383
t = Tempfile.new('INDEX')
384
t.close
385
tmp = t.path
386
387
if File.exist?(index_file)
388
if !File.writable?(index_file)
389
STDERR.puts "index file #{index_file} not writable!"
390
raise IndexFileError, "index generation error"
391
end
392
else
393
dir = File.dirname(index_file)
394
395
if !File.writable?(dir)
396
STDERR.puts"index file directory #{dir} not writable!"
397
raise IndexFileError, "index generation error"
398
end
399
end
400
401
if fetch
402
system "cd #{abs_ports_dir} && make fetchindex && cp #{index_file} #{tmp}"
403
else
404
system "cd #{abs_ports_dir} && make INDEXDIR=#{File.dirname(tmp)} INDEXFILE=#{File.basename(tmp)} index"
405
end
406
407
if File.zero?(tmp)
408
if fetch
409
STDERR.puts 'failed to fetch INDEX!'
410
raise IndexFileFetchError, "index fetch error"
411
else
412
STDERR.puts 'failed to generate INDEX!'
413
raise IndexFileError, "index generation error"
414
end
415
end
416
417
begin
418
File.chmod(0644, tmp)
419
rescue => e
420
STDERR.puts e.message
421
raise IndexFileError, "index chmod error"
422
end
423
424
if not system('/bin/mv', '-f', tmp, index_file)
425
STDERR.puts 'failed to overwrite #{index_file}!"'
426
raise IndexFileError, "index overwrite error"
427
end
428
429
STDERR.puts "done"
430
431
@categories = nil
432
@virtual_categories = nil
433
@origins = nil
434
@pkgnames = nil
435
@origins_by_categories = {}
436
@ports = {}
437
438
close_db
439
end
440
441
def open_db
442
@db and return @db
443
444
update_db
445
446
retried = false
447
448
begin
449
open_db_for_read!
450
451
check_db_version or raise TypeError, 'database version mismatch/bump detected'
452
453
s = @db[':categories']
454
s.is_a?(String) or raise TypeError, "missing key: categories"
455
@categories = s.split
456
457
s = @db[':virtual_categories']
458
s.is_a?(String) or raise TypeError, "missing key: virtual_categories"
459
@virtual_categories = s.split
460
461
s = @db[':origins']
462
s.is_a?(String) or raise TypeError, "missing key: origins"
463
@origins = s.split
464
465
s = @db[':pkgnames']
466
s.is_a?(String) or raise TypeError, "missing key: pkgnames"
467
@pkgnames = s.split.map { |n| PkgInfo.new(n) }
468
469
@origins_by_categories = {}
470
(@categories + @virtual_categories).each do |c|
471
s = @db['?' + c] and @origins_by_categories[c] = s.split
472
end
473
rescue => e
474
if retried
475
raise DBError, "#{e.message}: Cannot read the portsdb!"
476
end
477
478
STDERR.print "[#{e.message}] "
479
update_db(true)
480
481
retried = true
482
retry
483
end
484
485
@ports = {}
486
487
@db
488
rescue => e
489
STDERR.puts e.message
490
raise DBError, 'database file error'
491
end
492
493
def date_index
494
latest_mtime = File.mtime(index_file) rescue nil
495
@alt_index_files.each do |f|
496
mt = File.mtime(f)
497
latest_mtime = mt if mt > latest_mtime
498
end
499
latest_mtime
500
end
501
502
def date_db
503
File.mtime(@db_file) rescue nil
504
end
505
506
def up_to_date?
507
d1 = date_db() and d2 = date_index() and d1 >= d2
508
end
509
510
def select_db_dir(force = false)
511
return db_dir if File.writable?(db_dir)
512
513
db_dir_list.each do |dir|
514
set_db_dir(dir)
515
516
!force && up_to_date? and return dir
517
518
File.writable?(dir) and return dir
519
end
520
521
nil
522
end
523
524
def update_db(force = false)
525
if not File.exist?(index_file)
526
begin
527
update(true)
528
rescue IndexFileFetchError
529
update(false)
530
end
531
end
532
533
!force && up_to_date? and return false
534
535
close_db
536
537
select_db_dir(force) or raise "No directory available for portsdb!"
538
539
prev_sync = STDERR.sync
540
STDERR.sync = true
541
542
STDERR.printf "[Updating the portsdb <format:%s> in %s ... ", db_driver, db_dir
543
544
nports = `wc -l #{index_file}`.to_i
545
@alt_index_files.each do |f|
546
nports += `wc -l #{f}`.to_i
547
end
548
549
STDERR.printf "- %d port entries found ", nports
550
551
i = -1
552
553
@origins = []
554
@pkgnames = []
555
556
try_again = false
557
begin
558
open_db_for_rebuild!
559
560
index_files = shelljoin(index_file) + ' '
561
index_files.concat(@alt_index_files.join(' '))
562
563
open("| sort #{index_files}", 'r:utf-8') do |f|
564
f.each_with_index do |line, i|
565
lineno = i + 1
566
567
if lineno % 100 == 0
568
if lineno % 1000 == 0
569
STDERR.print lineno
570
else
571
STDERR.putc(?.)
572
end
573
end
574
575
begin
576
port_info = PortInfo.new(line)
577
578
next if ignore_category?(port_info.category)
579
580
origin = port_info.origin
581
pkgname = port_info.pkgname
582
583
port_info.categories.each do |category|
584
if @origins_by_categories.key?(category)
585
@origins_by_categories[category] << origin
586
else
587
@origins_by_categories[category] = [origin]
588
end
589
end
590
591
@ignore_categories.each do |category|
592
@origins_by_categories.delete(category)
593
end
594
595
@origins << origin
596
@pkgnames << pkgname
597
598
@db[origin] = port_info
599
@db[pkgname.to_s] = origin
600
rescue => e
601
STDERR.puts index_file + ":#{lineno}:#{e.message}"
602
end
603
end
604
end
605
606
STDERR.print ' '
607
608
real_categories = real_categories! | @extra_categories
609
all_categories = @origins_by_categories.keys
610
611
@categories = (real_categories - @ignore_categories).sort
612
@virtual_categories = (all_categories - real_categories).sort
613
614
@db[':categories'] = @categories.join(' ')
615
STDERR.putc(?.)
616
@db[':virtual_categories'] = @virtual_categories.join(' ')
617
STDERR.putc(?.)
618
@db[':origins'] = @origins.join(' ')
619
STDERR.putc(?.)
620
@db[':pkgnames'] = @pkgnames.map { |n| n.to_s }.join(' ')
621
STDERR.putc(?.)
622
all_categories.each do |c|
623
@db['?' + c] = @origins_by_categories[c].join(' ')
624
end
625
STDERR.putc(?.)
626
@db[':db_version'] = Marshal.dump(DB_VERSION)
627
rescue => e
628
if File.exist?(@db_file)
629
begin
630
STDERR.puts " error] Remove and try again."
631
File.unlink(@db_file)
632
try_again = true
633
rescue => e
634
raise DBError, "#{e.message}: Cannot update the portsdb! (#{@db_file})]"
635
end
636
else
637
raise DBError, "#{e.message}: Cannot update the portsdb! (#{@db_file})]"
638
end
639
ensure
640
close_db
641
end
642
643
if try_again
644
update_db(force)
645
else
646
STDERR.puts " done]"
647
STDERR.sync = prev_sync
648
true
649
end
650
651
end
652
653
def port(key)
654
key.is_a?(PortInfo) and return key
655
656
@ports.key?(key) and return @ports[key]
657
658
open_db
659
660
if key.include?('/')
661
val = @db[key]
662
elsif val = @db[key]
663
return port(val)
664
end
665
666
@ports[key] = if val then PortInfo.new(val) else nil end
667
end
668
alias [] port
669
670
def ports(keys)
671
keys.map { port(key) }
672
end
673
674
alias indices ports
675
676
def origin(key)
677
if p = port(key)
678
p.origin
679
else
680
nil
681
end
682
end
683
684
def origins(category = nil)
685
open_db
686
687
if category
688
@origins_by_categories[category]
689
else
690
@origins
691
end
692
end
693
694
def origins!(category = nil)
695
if category
696
# only lists the ports which primary category is the given category
697
subdirs(portdir(category)).map { |i|
698
File.join(category, i)
699
}
700
else
701
list = []
702
703
categories!.each do |i|
704
list.concat(origins!(i))
705
end
706
707
list
708
end
709
end
710
711
def each(category = nil)
712
ports = origins(category) or return nil
713
714
ports.each { |key|
715
yield(@db[key])
716
}
717
end
718
719
def each_category
720
categories.each { |key|
721
yield(key)
722
}
723
end
724
725
def each_origin(category = nil)
726
ports = origins(category) or return nil
727
728
ports.each { |key|
729
yield(key)
730
}
731
end
732
733
def each_origin!(category = nil, &block)
734
if category
735
# only lists the ports which primary category is the given category
736
subdirs(portdir(category)).each do |i|
737
block.call(File.join(category, i))
738
end
739
else
740
categories!.each do |i|
741
each_origin!(i, &block)
742
end
743
end
744
end
745
746
def each_pkgname
747
open_db
748
749
@pkgnames.each { |key|
750
yield(key)
751
}
752
end
753
754
def glob(pattern = '*')
755
list = []
756
pkg = nil
757
758
open_db
759
760
case pattern
761
when Regexp
762
is_port = pattern.source.include?('/')
763
else
764
if /^[<>]/ =~ pattern
765
raise "Invalid glob pattern: #{pattern}"
766
end
767
768
is_port = pattern.include?('/')
769
770
# shortcut
771
if portinfo = port(pattern)
772
if block_given?
773
yield(portinfo)
774
return nil
775
else
776
return [portinfo]
777
end
778
end
779
end
780
781
if is_port
782
@origins.each do |origin|
783
case pattern
784
when Regexp
785
next if pattern !~ origin
786
else
787
next if not File.fnmatch?(pattern, origin, File::FNM_PATHNAME)
788
end
789
790
if portinfo = port(origin)
791
if block_given?
792
yield(portinfo)
793
else
794
list.push(portinfo)
795
end
796
end
797
end
798
else
799
@pkgnames.each do |pkgname|
800
next if not pkgname.match?(pattern)
801
802
if portinfo = port(pkgname.to_s)
803
if block_given?
804
yield(portinfo)
805
else
806
list.push(portinfo)
807
end
808
end
809
end
810
end
811
812
if block_given?
813
nil
814
else
815
list
816
end
817
rescue => e
818
STDERR.puts e.message
819
820
if block_given?
821
return nil
822
else
823
return []
824
end
825
end
826
827
def exist?(port, quick = false)
828
return if %r"^[^/]+/[^/]+$" !~ port
829
830
dir = portdir(port)
831
832
return false if not File.file?(File.join(dir, 'Makefile'))
833
834
return true if quick
835
836
make_var('PKGNAME', dir) || false
837
end
838
839
def all_depends_list!(origin, before_args = nil, after_args = nil)
840
`cd #{$portsdb.portdir(origin)} && #{before_args || ''} make #{after_args || ''} all-depends-list`.lines.map { |line|
841
strip(line.chomp, true)
842
}.compact
843
end
844
845
def all_depends_list(origin, before_args = nil, after_args = nil)
846
if !before_args && !after_args && i = port(origin)
847
i.all_depends.map { |n| origin(n) }.compact
848
else
849
all_depends_list!(origin, before_args, after_args)
850
end
851
end
852
853
def masters(port)
854
dir = portdir(port)
855
856
ports = []
857
858
`cd #{dir} ; make -dd -n 2>&1`.each do |line|
859
if /^Searching for .*\.\.\.Caching .* for (\S+)/ =~ line.chomp
860
path = File.expand_path($1)
861
862
if (path.sub!(%r"^#{Regexp.quote(ports_dir)}/", '') ||
863
path.sub!(%r"^#{Regexp.quote(abs_ports_dir)}/", '')) &&
864
%r"^([^/]+/[^/]+)" =~ path
865
x = $1
866
867
ports << x if exist?(x) && !ports.include?(x)
868
end
869
end
870
end
871
872
ports.delete(port)
873
874
ports
875
end
876
877
def latest_link(port)
878
dir = portdir(port)
879
880
flag, name = make_var(['NO_LATEST_LINK', 'LATEST_LINK'], dir)
881
882
if flag
883
nil
884
else
885
name
886
end
887
end
888
889
def sort(ports)
890
tsort = PkgTSort.new
891
892
ports.each do |p|
893
portinfo = port(p)
894
895
portinfo or next
896
897
o = portinfo.origin
898
899
deps = all_depends_list(o) # XXX
900
901
tsort.add(o, *deps)
902
end
903
904
tsort.tsort! & ports
905
end
906
907
def sort!(ports)
908
ports.replace(sort(ports))
909
end
910
911
def recurse(portinfo, recurse_down = false, recurse_up = false)
912
if not portinfo.is_a?(PortInfo)
913
portinfo = port(portinfo)
914
end
915
916
list = []
917
918
portinfo or return list
919
920
if recurse_up
921
portinfo.required_depends.map do |name|
922
i = port(name)
923
924
list << i if i
925
end
926
end
927
928
list << portinfo
929
930
if recurse_down
931
# slow!
932
pkgname = portinfo.pkgname.fullname
933
934
glob do |i|
935
list << i if i.required_depends.include?(pkgname)
936
end
937
end
938
939
list
940
end
941
end
942
943