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,330 @@
1
+ require 'aur/helpers'
2
+ require 'aur/packages'
3
+ require 'aur/install_packages'
4
+ require 'aur/makepkg'
5
+
6
+ module Archlinux
7
+
8
+ class Config
9
+ def self.create(v)
10
+ v.is_a?(self) ? v : self.new(v)
11
+ end
12
+
13
+ attr_accessor :opts
14
+ Archlinux.delegate_h(self, :@opts)
15
+ include SH::ShConfig
16
+ include DR::PPHelper
17
+
18
+ # pass nil to prevent loading a config file
19
+ def initialize(file="aur.rb", **opts)
20
+ @file=file
21
+ if file
22
+ file=Pathname.new(file)
23
+ file=Pathname.new(ENV["XDG_CONFIG_HOME"] || "#{ENV['HOME']}/.config") + file if file.relative?
24
+ end
25
+ file_config= file&.readable? ? file.read : (SH.logger.error "Error: Config file '#{file}' unreadable" unless file.nil?; '{}')
26
+ wrap=eval("Proc.new { |config| #{file_config} }")
27
+ @opts=default_config.deep_merge(opts)
28
+ user_conf=wrap.call(self)
29
+ @opts.deep_merge!(user_conf) if user_conf.is_a?(Hash)
30
+ end
31
+
32
+ def to_pp
33
+ @file.to_s
34
+ end
35
+
36
+ def sh_config
37
+ @opts[:sh_config]
38
+ end
39
+
40
+ def default_config
41
+ {
42
+ cache: "arch_aur", #where we dl PKGBUILDs; if relative will be in XDG_CACHE_HOME
43
+ db: 'aur', #if relative the db will be in cache dir
44
+ aur_url: "https://aur.archlinux.org/", #base aur url
45
+ chroot: {
46
+ root: "/var/lib/aurbuild/x86_64", #chroot root
47
+ active: false, #do we use the chroot?
48
+ update: 'pacman -Syu --noconfirm', #how to update an existing chroot
49
+ packages: ['base-devel'], #the packages that are installed in the chroto
50
+ # It can make sense to add %w(python ruby nodejs go-pie rust git)...
51
+ },
52
+ default_packages_class: PackageList,
53
+ # default_install_list_class: AurMakepkgCache,
54
+ default_install_packages_class: AurPackageList,
55
+ default_install_list_class: AurCache,
56
+ default_get_class: Git, #we use git to fetch PKGBUILD from aur
57
+ sign: true, #can be made more atomic, cf the sign method
58
+ config_files: {
59
+ default: {
60
+ pacman: "/etc/pacman.conf", #default pacman-conf
61
+ makepkg: "/etc/makepkg.conf",
62
+ },
63
+ chroot: {
64
+ pacman: "/usr/share/devtools/pacman-extra.conf", #pacman.conf for chroot build
65
+ makepkg: "/usr/share/devtools/makepkg-x86_64.conf",
66
+ },
67
+ local: {
68
+ pacman: "/etc/pacman.conf", # pacman-conf for local makepkg build
69
+ },
70
+ },
71
+ sh_config: { #default programs options, called each time
72
+ makepkg: {default_opts: []},
73
+ makechrootpkg: {default_opts: ["-cu"]},
74
+ # So on fist thought, we do not need -u since we update 'root' before ourselves; but if we build several packages we may need the previous ones in the db, and since we only update 'root' once, they won't be available on 'copy'; so we still need '-u'
75
+ },
76
+ makepkg: {
77
+ build_args: ["-crs", "--needed"], #only used when building
78
+ },
79
+ view: "vifm -c view! -c tree -c 0", #can also be a Proc
80
+ sudo_loop: {
81
+ command: "sudo -v",
82
+ interval: 30,
83
+ active: true,
84
+ }
85
+ }
86
+ end
87
+
88
+ # packages to check
89
+ def default_packages(use_db: db != false, use_foreign: true)
90
+ if @default_packages.nil?
91
+ # by default this is the db packages + foreign packages
92
+ default=use_db ? db.packages : to_packages([])
93
+ default.merge(RepoPkgs.new(Repo.foreign_list, config: self).packages) if use_foreign
94
+ default=yield default if block_given?
95
+ @default_packages=to_packages(default.l, install: true)
96
+ else
97
+ @default_packages
98
+ end
99
+ end
100
+
101
+ def get_config_file(name, type: :default)
102
+ dig(:config_files, type, name) || dig(:config_files, :default, name)
103
+ end
104
+
105
+ def view(dir)
106
+ view=@opts[:view]
107
+ SH.sh_or_proc(view, dir)
108
+ end
109
+
110
+ def cachedir
111
+ global_cache=Pathname.new(ENV["XDG_CACHE_HOME"] || "#{ENV['HOME']}/.cache")
112
+ cache=global_cache+@opts[:cache]
113
+ cache.mkpath
114
+ cache
115
+ end
116
+
117
+ # add our default db to the list of repos
118
+ private def setup_pacman_conf(conf)
119
+ pacman=PacmanConf.create(conf, config: self)
120
+ aur=self.db(false)
121
+ if aur and !pacman[:repos].include?(aur.repo_name)
122
+ require 'uri'
123
+ pacman[:repos][aur.repo_name]={Server: ["file://#{DR::URIEscape.escape(aur.dir.to_s)}"]}
124
+ end
125
+ pacman
126
+ end
127
+
128
+ def default_pacman_conf
129
+ @default_pacman_conf ||= if (conf=get_config_file(:pacman))
130
+ PacmanConf.new(conf, config: self)
131
+ else
132
+ PacmanConf.new(config: self)
133
+ end
134
+ end
135
+
136
+ def chroot_devtools
137
+ unless @chroot_devtools
138
+ devtools_pacman=PacmanConf.new(get_config_file(:pacman, type: :chroot))
139
+ makepkg_conf=get_config_file(:makepkg, type: :chroot)
140
+ my_pacman=default_pacman_conf
141
+ devtools_pacman[:repos].merge!(my_pacman.non_official_repos)
142
+ devtools_pacman=setup_pacman_conf(devtools_pacman)
143
+ @chroot_devtools = Devtools.new(pacman_conf: devtools_pacman, makepkg_conf: makepkg_conf, config: self)
144
+ end
145
+ @chroot_devtools
146
+ end
147
+
148
+ def local_devtools
149
+ unless @local_devtools
150
+ makepkg_pacman=get_config_file(:pacman, type: :local)
151
+ makepkg_conf=get_config_file(:makepkg, type: :local)
152
+ makepkg_pacman=setup_pacman_conf(makepkg_pacman)
153
+ @local_devtools = Devtools.new(pacman_conf: makepkg_pacman, makepkg_conf: makepkg_conf, config: self)
154
+ end
155
+ @local_devtools
156
+ end
157
+
158
+ def db=(name)
159
+ case name
160
+ when DB
161
+ @db=name
162
+ when Pathname
163
+ @db=DB.new(name, config: self)
164
+ when String
165
+ if DB.db_file?(name)
166
+ @db=DB.new(name, config: self)
167
+ else
168
+ @db=DB.new(cachedir+".db"+"#{name}.db.tar.gz", config: self)
169
+ end
170
+ when true
171
+ @db=DB.new(cachedir+".db"+"aur.db.tar.gz", config: self)
172
+ when false, nil
173
+ @db=name #false for false, nil to reset
174
+ else
175
+ SH.logger.warn("Database name #{name} not suitable, fallback to default")
176
+ @db=nil
177
+ end
178
+ # reset these so the pacman_conf gets the correct db name
179
+ @makepkg_config=nil
180
+ @devtools_config=nil
181
+ end
182
+
183
+ def db(create=true)
184
+ @db.nil? and self.db=@opts[:db]
185
+ if create and @db
186
+ @db.create
187
+ end
188
+ @db
189
+ end
190
+
191
+ # if changing a setting, we may need to reset the rest
192
+ def reset
193
+ self.db=nil
194
+ end
195
+
196
+ # note: since 'true' is frozen, we cannot extend it and keep a
197
+ # @sudo_loop_thread. Moreover we only want one sudo loop active, so we
198
+ # will call it ourselves
199
+ def sudo(arg=true)
200
+ if dig(:sudo_loop, :active)
201
+ opts=dig(:sudo_loop).clone
202
+ opts.delete(:active)
203
+ self.extend(SH::SudoLoop.configure(**opts))
204
+ self.sudo_loop
205
+ end
206
+ arg
207
+ end
208
+
209
+ def stop_sudo_loop
210
+ stop_sudo_loop if respond_to?(:stop_sudo_loop)
211
+ end
212
+
213
+ def to_packages(l=[], install: false)
214
+ Archlinux.create_class(
215
+ install ? @opts[:default_install_packages_class] :
216
+ @opts[:default_packages_class],
217
+ l, config: self)
218
+ end
219
+
220
+ #:package, :db
221
+ def use_sign?(mode)
222
+ opt_sign=@opts[:sign]
223
+ if opt_sign.is_a?(Hash)
224
+ opt_sign[mode]
225
+ else
226
+ opt_sign
227
+ end
228
+ end
229
+
230
+ # output a list of sign names, or [] if we want to sign with the default sign name, false if we don't
231
+ def sign_names
232
+ opt_sign=@opts[:sign]
233
+ signs=if opt_sign.is_a?(Hash)
234
+ opt_sign.values
235
+ else
236
+ [*opt_sign]
237
+ end
238
+ names=signs.select {|s| s.is_a?(String)}
239
+ names = false if names.empty? and ! signs.any?
240
+ return names
241
+ end
242
+
243
+ # return the files that were signed
244
+ def sign(*files, sign_name: nil, force: false)
245
+ sign_name=use_sign?(sign_name) if sign_name.is_a?(Symbol)
246
+ files.map do |file|
247
+ sig="#{file}.sig"
248
+ if !Pathname.new(file).file?
249
+ SH.logger.error "Invalid file to sign #{file}"
250
+ next
251
+ end
252
+ if Pathname.new(sig).file?
253
+ if force
254
+ SH.logger.verbose2 "Signature #{sig} already exits, overwriting"
255
+ else
256
+ SH.logger.verbose2 "Signature #{sig} already exits, skipping"
257
+ next
258
+ end
259
+ end
260
+ args=['--detach-sign', '--no-armor']
261
+ args+=['-u', sign_name] if sign_name.is_a?(String)
262
+ launch(:gpg, *args, file) do |*args|
263
+ suc, _r=SH.sh(*args)
264
+ suc ? file : nil
265
+ end
266
+ end.compact
267
+ end
268
+
269
+ def verify_sign(*files)
270
+ args=['--verify']
271
+ files.map do |file|
272
+ file="#{file}.sig" unless file.to_s =~ /(.sig|.asc)/
273
+ launch(:gpg, *args, file) do |*args|
274
+ suc, _r=SH.sh(*args)
275
+ [file, suc]
276
+ end
277
+ end.to_h
278
+ end
279
+
280
+ def install_list
281
+ @install_list ||= Archlinux.create_class(@opts[:default_install_list_class], config: self)
282
+ end
283
+
284
+ def pre_install(*args, **opts)
285
+ names=sign_names
286
+ if names
287
+ cargs=["--detach-sign", "-o", "/dev/null", "/dev/null"]
288
+ if names.empty?
289
+ launch(:gpg, *cargs)
290
+ else
291
+ args=names.map { |name| ['-u', name] }.flatten+cargs
292
+ launch(:gpg, *args)
293
+ end
294
+ end
295
+ end
296
+
297
+ def post_install(pkgs, install: false, **opts)
298
+ if (db=self.db)
299
+ if install
300
+ tools=local_devtools
301
+ # info=opts[:pkgs_info]
302
+ # to_install=info[:all_pkgs].select {|_k,v| v[:op]==:install}.
303
+ # map {|_k,v| v[:out_pkg]}
304
+ # tools.sync_db(db.repo_name, install: to_install)
305
+
306
+ # Let's just install anything and let pacman handle it
307
+ m=opts[:makepkg_list]
308
+ if m
309
+ # we need to update the package versions with the ones
310
+ # provided by the current makepkg (which may be more recent than
311
+ # the one from aur's rpc in case of devel packages)
312
+ ipkgs=pkgs.map do |pkg|
313
+ found=m.packages.find(Query.strip(pkg))
314
+ found || pkg
315
+ end
316
+ else
317
+ ipkgs=pkgs
318
+ end
319
+ tools.sync_db(db.repo_name, install: %w(--needed) + ipkgs)
320
+ end
321
+ end
322
+ end
323
+
324
+ def parser(parser) #to add cli options in the config file
325
+ end
326
+ end
327
+
328
+ self.singleton_class.attr_accessor :config
329
+ @config ||= Config.new("aur.rb")
330
+ end
@@ -0,0 +1,328 @@
1
+ require 'aur/config'
2
+ require 'aur/packages'
3
+ require 'aur/repos'
4
+ require 'time'
5
+
6
+ module Archlinux
7
+ class DB
8
+ extend CreateHelper
9
+
10
+ def self.db_file?(name)
11
+ case name
12
+ when Pathname
13
+ true
14
+ when String
15
+ if name.include?('/') or name.match(/\.db(\..*)?$/)
16
+ true
17
+ else
18
+ false #Usually we assume this is a repo name
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_accessor :file, :config
24
+ def initialize(file, config: Archlinux.config)
25
+ @orig_file=Pathname.new(file)
26
+ @file=@orig_file.realdirpath rescue @orig_file.abs_path
27
+ @config=config
28
+ end
29
+
30
+ def mkpath
31
+ @file.dirname.mkpath
32
+ end
33
+
34
+ def path
35
+ if file.exist?
36
+ file.realpath
37
+ else
38
+ file
39
+ end
40
+ end
41
+
42
+ def repo_name
43
+ @file.basename.to_s.sub(/\.db(\..*)?$/,'')
44
+ end
45
+
46
+ def create
47
+ mkpath
48
+ unless @file.exist?
49
+ call(:'repo-add', path)
50
+ end
51
+ self
52
+ end
53
+
54
+ def to_s
55
+ @file.to_s
56
+ end
57
+
58
+ # a db is a tar.gz archive of packages/desc, like yay-8.998-1/descr
59
+ def bsdcat_list
60
+ res= SH.run_simple("bsdcat #{@file.shellescape}", chomp: :lines) {return nil}
61
+ list=[]; pkg={}; mode=nil
62
+ flush = lambda do
63
+ # catch old deps files which don't specify the full infos
64
+ unless pkg[:name].nil? and pkg[:base].nil?
65
+ pkg[:repo]||=path
66
+ list << pkg
67
+ end
68
+ end
69
+ res.each do |l|
70
+ next if l.empty? or l.match(/^\u0000*$/)
71
+ if (m=l.match(/(\u0000+.*\u0000+)?%([A-Z0-9]*)%$/))
72
+ mode=m[2].downcase.to_sym
73
+ if m[1] #new db entry
74
+ flush.call #store old db entry
75
+ pkg={}
76
+ end
77
+ else
78
+ l=l.to_i if mode==:csize or mode==:isize
79
+ l=Time.at(l.to_i) if mode==:builddate
80
+ Archlinux.add_to_hash(pkg, mode, l)
81
+ end
82
+ end
83
+ flush.call #don't forget the last one
84
+ list
85
+ end
86
+
87
+ def list
88
+ res= SH.run_simple("bsdtar -xOf #{@file.shellescape} '*/desc'", chomp: :lines) {return nil}
89
+ list=[]; pkg={}; mode=nil
90
+ flush = lambda do
91
+ unless pkg.empty?
92
+ pkg[:repo]||=path
93
+ list << pkg
94
+ end
95
+ end
96
+ res.each do |l|
97
+ next if l.empty?
98
+ if (m=l.match(/^%([A-Z0-9]*)%$/))
99
+ mode=m[1].downcase.to_sym
100
+ if mode==:filename #new db entry
101
+ flush.call #store old db entry
102
+ pkg={}
103
+ end
104
+ else
105
+ l=l.to_i if mode==:csize or mode==:isize
106
+ l=Time.at(l.to_i) if mode==:builddate
107
+ Archlinux.add_to_hash(pkg, mode, l)
108
+ end
109
+ end
110
+ flush.call #don't forget the last one
111
+ list
112
+ end
113
+
114
+ def files(absolute=true)
115
+ list.map do |pkg|
116
+ file=Pathname.new(pkg[:filename])
117
+ absolute ? dir + file : file
118
+ end
119
+ end
120
+ # def files
121
+ # packages.l.map {|pkg| dir+Pathname.new(pkg[:filename])}
122
+ # end
123
+
124
+ def dir
125
+ # # we memoize this because if we get called again in a dir.chdir
126
+ # # call, then the realpath will fail
127
+ # @dir ||= @file.dirname.realpath
128
+ @file.dirname
129
+ end
130
+
131
+ def call(*args, **opts)
132
+ @config.launch(*args, **opts) do |*a, **o|
133
+ dir.chdir do
134
+ SH.sh(*a, **o)
135
+ end
136
+ end
137
+ end
138
+
139
+ def move_to_db(*files, op: :mv)
140
+ files=files.map {|f| Pathname.new(f).realpath}
141
+ dir=self.dir
142
+ SH.logger.verbose "#{op}: #{files.join(', ')} to #{dir}"
143
+ files.map do |f|
144
+ if f.dirname == dir
145
+ SH.logger.verbose2 "! #{f} already exists in #{dir}"
146
+ f
147
+ else
148
+ new=dir+f.basename
149
+ SH.logger.verbose2 "-> #{op} #{f} to #{new}"
150
+ f.send(op, new)
151
+ sig=Pathname.new(f.to_s+".sig") #mv .sig too
152
+ if sig.exist?
153
+ newsig=dir+sig.basename
154
+ SH.logger.verbose2 "-> #{op} #{sig} to #{newsig}"
155
+ sig.send(op, newsig)
156
+ end
157
+ new
158
+ end
159
+ end
160
+ end
161
+
162
+ def add(*files, cmd: :'repo-add', default_opts:[], sign: @config&.use_sign?(:db), force_sign: false, **opts)
163
+ default_opts+=['-s', '-v'] if sign
164
+ default_opts+=['--key', sign] if sign.is_a?(String)
165
+ dir.chdir do
166
+ files.map! {|f| Pathname.new(f)}
167
+ existing_files=files.select {|f| f.file?}
168
+ missing_files = files-existing_files
169
+ SH.logger.warn "In #{cmd}, missing files: #{missing_files.join(', ')}" unless missing_files.empty?
170
+ unless existing_files.empty?
171
+ sign_files = @config&.use_sign?(:package)
172
+ PackageFiles.new(*existing_files, config: @config).sign(sign_name: sign_files, force: force_sign) if sign_files
173
+ call(cmd, path, *existing_files, default_opts: default_opts, **opts)
174
+ @packages=nil #we need to refresh the list
175
+ end
176
+ end
177
+ files
178
+ end
179
+
180
+ def remove(*pkgnames, cmd: :'repo-remove', default_opts:[], sign: @config&.use_sign?(:db), **opts)
181
+ default_opts+=['-s', '-v'] if sign
182
+ default_opts+=['--key', sign] if sign.is_a?(String)
183
+ dir.chdir do
184
+ call(cmd, path, *pkgnames, default_opts: default_opts, **opts)
185
+ @packages=nil #we need to refresh the list
186
+ end
187
+ pkgnames
188
+ end
189
+
190
+ def packages(refresh=false)
191
+ @packages=nil if refresh
192
+ # @packages||=PackageList.new(list, config: @config)
193
+ @packages||=@config.to_packages(list)
194
+ end
195
+
196
+ def dir_packages_cls
197
+ PackageFiles.from_dir(dir, config: @config)
198
+ end
199
+ def dir_packages
200
+ dir_packages_cls.packages
201
+ end
202
+
203
+ def package_files_cls
204
+ PackageFiles.new(*files, config: @config)
205
+ end
206
+ def package_files
207
+ package_files_cls.packages
208
+ end
209
+
210
+ # sign the files in the db (return the list of signed file, by default
211
+ # unless force: true is passed this won't resign already signed files)
212
+ def sign_files(sign_name: :package, **opts)
213
+ @config&.sign(*files, sign_name: sign_name, **opts)
214
+ end
215
+ def sign_db(sign_name: :db, **opts)
216
+ @config&.sign(@file, sign_name: sign_name, **opts) if @file.file?
217
+ end
218
+
219
+ def verify_sign_db
220
+ @config&.verify_sign(@file)
221
+ end
222
+ def verify_sign_files
223
+ @config&.verify_sign(*files)
224
+ end
225
+ # check the inline signatures
226
+ def verify_sign_pkgs(*pkgs)
227
+ packages.get_packages(*pkgs).map do |pkg|
228
+ pgpsign=pkg[:pgpsig]
229
+ if pgpsign
230
+ require 'base64'
231
+ sig=Base64.decode64(pgpsign)
232
+ filename=dir+pkg[:filename]
233
+ @config.launch(:gpg, "--enable-special-filenames", "--verify", "-", filename, mode: :capture, stdin_data: sig) do |*args|
234
+ suc, _r=SH.sh(*args)
235
+ [pkg.name, suc]
236
+ end
237
+ else
238
+ [pkg.name, false]
239
+ end
240
+ end.to_h
241
+ end
242
+
243
+ # if we missed some signatures, resign them (and add them back to the
244
+ # db to get the signatures in the db). This override the sign:false
245
+ # config options.
246
+ def resign(**opts)
247
+ signed_files=sign_files(**opts)
248
+ add(*signed_files) #note that this may try to sign again, but since the signature exists it won't launch gpg
249
+ sign_db(**opts) #idem, normally the db will be signed by repo-add -v, but in case the signature was turned off in the config, this forces the signature
250
+ end
251
+
252
+ def check
253
+ packages.same?(package_files)
254
+ end
255
+
256
+ def check_update(other=dir_packages)
257
+ self.packages.check_updates(other)
258
+ # yield up if block_given?
259
+ # refresh=up.select {|_k, u| u[:op]==:upgrade or u[:op]==:downgrade}
260
+ # add=up.select {|_k, u| u[:op]==:install}
261
+ # remove=up.select {|_k, u| u[:op]==:obsolete}
262
+ # return {refresh: refresh, add: add, remove: remove}
263
+ end
264
+
265
+ def show_updates(other=dir_packages, **showopts)
266
+ c=check_update(other)
267
+ packages.show_updates(c, **showopts)
268
+ c
269
+ end
270
+
271
+ def update(other=dir_packages, add_for: %i(upgrade downgrade install), rm_for: %i(obsolete), **showopts)
272
+ c=show_updates(other, **showopts)
273
+ to_add=c.select {|_k, u| add_for.include?(u[:op])}
274
+ to_rm=c.select {|_k, u| rm_for.include?(u[:op])}
275
+ add(*to_add.map { |_k,v| other[v[:out_pkg]].path })
276
+ # remove(*(r[:remove].map {|_k,v| packages[v[:in_pkg]].file.shellescape}))
277
+ remove(* to_rm.map {|_k,v| Query.strip(v[:in_pkg])})
278
+ c
279
+ end
280
+
281
+ # move/copy files to db and add them
282
+ # if update==true, only add more recent packages
283
+ # pkgs should be a PackageFiles or a PackageList or a list of files
284
+ def add_to_db(pkgs, update: true, op: :cp)
285
+ if update
286
+ pkgs=PackageFiles.create(pkgs).packages unless pkgs.is_a? PackageList
287
+ up=self.packages.check_updates(pkgs)
288
+ pkgs=up.select {|_k, u| u[:op]==:upgrade or u[:op]==:install}.map do |_k,v|
289
+ pkgs[v[:out_pkg]].path
290
+ end
291
+ else
292
+ pkgs=pkgs.map {|_k,v| v.path } if pkgs.is_a?(PackageList)
293
+ end
294
+ SH.logger.mark "Updating #{pkgs} in #{self}"
295
+ cp_pkgs=move_to_db(*pkgs, op: op)
296
+ add(*cp_pkgs)
297
+ end
298
+
299
+ #remove from db and also remove the package files
300
+ def rm_from_db(*pkgs)
301
+ to_rm=packages.get_packages(*pkgs)
302
+ PackageFiles.rm_files(*to_rm.map {|pkg| pkg.path}, dir: self.dir)
303
+ remove(*pkgs)
304
+ end
305
+
306
+ # in clean_dir, we clean the dir packages which have a newer
307
+ # version (in the dir).
308
+ def clean_dir(dry_run: true)
309
+ files=dir_packages_cls
310
+ files.clean(dry_run: dry_run)
311
+ end
312
+
313
+ # In clean, we clean the dir packages which are newer or not present in
314
+ # the db. This is like the reverse of `update`. In particular be
315
+ # careful that this will delete newer versions or added versions
316
+ def clean(dry_run: true)
317
+ dir_pkgs=dir_packages_cls
318
+ dir_files=dir_pkgs.files
319
+ db_files=files
320
+ to_remove=dir_files - db_files
321
+ if dry_run
322
+ to_remove
323
+ else
324
+ PackageFiles.rm_files(*to_remove, dir: self.dir)
325
+ end
326
+ end
327
+ end
328
+ end