/
aurb.rb
executable file
·377 lines (311 loc) · 10.8 KB
/
aurb.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#!/usr/bin/env ruby
# frozen_string_literal: true
if Process.uid == 0
abort "Please don't run this script as root. The AUR is considered untrustworthy, and by extension so are the operations run upon it."
end
if RUBY_VERSION < "2.1.0"
abort "aurb requires Ruby >= 2.1.0."
end
if not `which pacman`
abort "Are you running this on Arch Linux? Pacman was not found."
end
if not `which git`
abort "Git must be installed for packages to be downloaded from the AUR."
end
require "open-uri"
require "json"
require "zlib"
require "fileutils"
%w(parseconfig).each do |gem|
begin
require gem
rescue LoadError
abort "`#{gem}' gem was not found. Please run `gem install #{gem}` to run aurb."
end
end
config_path = "#{ENV["HOME"]}/.config/aurb/aurb.conf"
if !File.exist?(config_path)
# create a default config
FileUtils.mkdir_p(File.dirname(config_path))
File.open(config_path, "w+") do |config_file|
config_file.write(<<-CONFIG.gsub(/^ {6}/, "").strip)
# Directory to save to
save_path = #{ENV["HOME"]}/AUR
# Packages to ignore in update checks
#ignore_pkg = package1 package2
CONFIG
end
end
VERSION = "v2.4.0".freeze
CONFIG = ParseConfig.new(config_path) rescue {"save_path" => "#{ENV["HOME"]}/AUR"}
AUR_URL = "https://aur.archlinux.org"
RPC_ENDPOINT = "#{AUR_URL}/rpc/?v=5&type=%s"
unless File.exist?(CONFIG["save_path"]) && File.writable?(CONFIG["save_path"])
warn("WARNING: Save path `#{CONFIG["save_path"]}' is not writable. Some actions, " \
"such as downloading, will not work. You can modify this in the config file.\n\n")
end
module Helpers
COLORS = %i(grey red green yellow blue purple cyan white).freeze
protected def color(text, effect)
if $stdout.tty? && ENV["TERM"]
return "\033[0;#{30 + COLORS.index(effect.to_sym)}m#{text}\033[0m"
end
text
end
# wrap this so we can gracefully handle connection issues
protected def GET(uri)
open(uri).read
rescue OpenURI::HTTPError
puts "\n#{color("x", :red)} URI not found (#{uri})"
exit 1
rescue SocketError
puts "\n#{color("x", :red)} Connection problem."
exit 1
end
protected def exec_cmd(*command)
puts color(" -> Running `#{command.join(" ")}`", :grey)
system(*command)
end
protected def prompt(question)
print " #{question} [Y/n] "
answer = $stdin.gets.chomp
answer = "Y" if answer.empty?
answer.upcase == "Y"
end
end
class Package
include Comparable
include Helpers
NUM_VOTES = "NumVotes".freeze
URL_PATH = "URLPath".freeze
NAME = "Name".freeze
VERSION = "Version".freeze
DESCRIPTION = "Description".freeze
DEPENDS = "Depends".freeze
attr_reader :name, :attributes
def initialize(name, attributes: nil)
@name = name
if Hash === attributes
@attributes = attributes
elsif attributes
retrieve_attributes
end
end
def retrieve_attributes
info_url = RPC_ENDPOINT % "info&arg=" + URI.escape(name)
json = JSON.parse(GET(info_url))
if json && json["resultcount"] > 0
@attributes = Array(json["results"])[0]
@attributes.each_key(&:freeze)
else
puts "\n#{color("!", :yellow)} Failed to retrieve attributes for #{name}. " \
"This usually means the package does not exist."
end
end
def version
if Hash(attributes)[VERSION]
return attributes[VERSION].split(/\D+/).map(&:to_i)
end
[0]
end
def dependencies
Array(Hash(attributes)[DEPENDS])
end
def <=>(other)
[version.size, other.version.size].max.times do |i|
cmp = version[i].to_i <=> other.version[i].to_i
return cmp if cmp != 0
end
0
end
end
class App
include Helpers
def optparse!(*argv)
print_help if argv.size < 1
case argv.shift
when "--install", "install"
if argv.any?
argv.each { |package| install(package) }
else print_help
end
when "--clean-install", "cleaninstall"
if argv.any?
argv.each { |package| install(package, clean_install: true) }
else print_help
end
when "-d", "--download", "download"
if argv.any?
argv.each { |package| download(package) }
else print_help
end
when "-s", "--search", "search"
if argv.any?
search(argv.join(" "))
else print_help
end
when "-i", "--info", "info"
if argv.any?
argv.each { |package| info(package) }
else print_help
end
when "-u", "--updates", "updates"
check_updates
when "-v", "--version", "version"
puts VERSION
else
print_help
end
end
def print_help
puts <<-HELP.gsub(/^ {6}/, "").strip
aurb.rb #{VERSION} (Ruby: #{RUBY_VERSION})
USAGE: #{$0} [action] [arg] ([action2] [arg2]...)
where action is one of:
-d, --download PKG download PKG into #{CONFIG["save_path"]}
--install PKG download, build, and install PKG
--clean-install PKG clean previous build(s), download and install PKG
-s, --search TERM search for TERM
-i, --info PKG print info about PKG
-u, --updates checks for updates to installed AUR packages
HELP
exit 1
end
def download(package_name)
if !File.exist?(CONFIG["save_path"]) || !File.directory?(CONFIG["save_path"])
puts color("Save path doesn't exist, or is not a directory.", :red)
return false
end
puts "#{color("::", :blue)} Downloading #{color(package_name, :cyan)} into #{CONFIG["save_path"]}... "
local_path = File.join(CONFIG["save_path"], package_name)
git_url = "#{AUR_URL}/#{package_name}.git"
if File.exist?(local_path) && File.directory?(local_path)
if exec_cmd("cd #{local_path} && git pull")
puts color("Successfully updated existing package build files.", :green)
return true
end
else
if exec_cmd("git clone #{git_url} #{local_path}")
puts color("Successfully downloaded package build files.", :green)
return true
end
end
false
end
def install(package_name, clean_install: false)
puts "#{color("::", :blue)} Installing #{color(package_name, :cyan)}... "
package = Package.new(package_name, attributes: true)
local_path = File.join(CONFIG["save_path"], package.name)
if clean_install
begin
FileUtils.remove_entry_secure(File.join(local_path, "*"))
rescue Errno::ENOENT
# Directories don't exist.
end
end
if !File.exist?(local_path) || !File.directory?(local_path) || clean_install
if package.dependencies.any?
packages_in_repos = `pacman -Sl`
aur_packages_installed = `pacman -Qqm`.split("\n")
# Select only packages that aren't in official repo's, and install them first.
deps_in_aur = package.dependencies.reject { |d| !!packages_in_repos[d] }
if deps_in_aur.any?
puts "#{color("==>", :green)} Found #{color(deps_in_aur.size, :blue)} AUR " \
"dependencies of #{color(package_name, :cyan)}: #{deps_in_aur.join(", ")}"
(deps_in_aur - aur_packages_installed).each do |dependency|
install(dependency)
end
end
end
download(package_name) or exit 1
end
Dir.chdir(local_path) do
if prompt("Edit PKGBUILD before building (#{color("RECOMMENDED", :green)})?")
exec_cmd("#{ENV["EDITOR"] || "vim"} PKGBUILD")
end
Dir["*.install"].each do |install_file|
if prompt("Edit #{File.basename(install_file)} before building (#{color("RECOMMENDED", :green)})?")
exec_cmd("#{ENV["EDITOR"] || "vim"} #{install_file}")
end
end
exec_cmd("makepkg", clean_install ? "-sfCi" : "-si")
end
rescue Interrupt
puts "\n #{color("x", :red)} Interrupted by user."
end
TIME_KEYS = %w(FirstSubmitted LastModified OutOfDate).freeze.map(&:freeze)
TIME_FORMAT = "%d/%m/%Y %H:%M".freeze
def info(package_name)
print "#{color("::", :blue)} Showing information for #{color(package_name, :cyan)}:"
package = Package.new(package_name, attributes: true)
puts "\n\n"
package.attributes.each do |key, value|
print color(key.rjust(16), :white)
if TIME_KEYS.include?(key)
value = Time.at(value.to_i).strftime(TIME_FORMAT) rescue value
end
value = value.join(", ") if value.is_a?(Array)
puts " " + value.to_s
end if package.attributes
end
def check_updates
puts "#{color("::", :blue)} Checking for updates...\n\n"
ignore_list = CONFIG["ignore_pkg"].to_s.split(" ")
local_aur_packages = `pacman -Qm`.split("\n").map(&:split).delete_if { |package_info|
name, version = package_info
# skip packages that are in community by now
in_community = Dir["/var/lib/pacman/sync/community/#{name}-#{version}"].any?
# skip packages that are ignored through config
in_ignore_list = ignore_list.include?(name)
in_community or in_ignore_list
}.map { |package_info|
name, version = package_info
Package.new(name, attributes: {Package::VERSION => version})
}
info_url = RPC_ENDPOINT % "multiinfo&arg[]=" + local_aur_packages.map { |package|
URI.escape(package.name)
}.join("&arg[]=")
json = JSON.parse(GET(info_url))
if json && json["resultcount"] > 0
local_aur_packages.each do |package|
latest_package = Package.new(package.name, attributes:
json["results"].find { |result| result[Package::NAME] == package.name }
)
if !(latest_package.attributes && latest_package.attributes.empty?) && package < latest_package
puts "#{color(">", :yellow)} %s has an update available (%s -> %s)\n" % [
color(package.name, :cyan),
color(package.attributes[Package::VERSION], :red),
color(latest_package.attributes[Package::VERSION], :green)
]
else
puts " #{color(package.name, :cyan)} #{package.attributes[Package::VERSION]} is up to date\n"
end
end
end
end
def search(term)
print "#{color("::", :cyan)} Searching for #{color(term, :cyan)}... "
search_url = RPC_ENDPOINT % "search&arg=" + URI.escape(term)
json = JSON.parse(GET(search_url))
if json && json["resultcount"] > 0
puts "Found #{json["resultcount"]} results:\n\n"
json["results"].sort { |a, b|
b[Package::NUM_VOTES] <=> a[Package::NUM_VOTES]
}.each do |result|
package = Package.new(result[Package::NAME], attributes: result)
begin
puts " %s %s (%d)\n %s" % [
color(package.attributes[Package::NAME], :cyan),
color(package.attributes[Package::VERSION], :green),
package.attributes[Package::NUM_VOTES],
package.attributes[Package::DESCRIPTION]
]
rescue Errno::EPIPE
end
end
else
puts "Failed to find any results."
end
end
end
App.new.optparse!(*ARGV.dup)