Newer
Older
#!/usr/bin/ruby
require 'optparse'
require 'readline'
$options = {}
OptionParser.new do |opts|
opts.banner = <<~END
Usage: #{File.basename($0)} [Git URI]
Maintains Git-source PKGBUILDs. Allows selecting a new commit to build.
If first source is not a git URI, tries to alter PKGBUILD to port from
autotools distributed source to a git repo. If an absolute Git URI is
not provided, tries discover it automatically.
Otherwise, if a Git URI is provided, replaces the first source with the
provided URI.
Submodules are not yet handled. Remember to properly source and inject
any submodules required.
Options:
END
opts.on("-h", "--help", "Print this help") do
puts opts
exit
end
end.parse!
ENV["GIT_TERMINAL_PROMPT"] = "0"
def header(s)
puts "\n\e[1m#{s}:\e[0m"
end
def indent(s, c=2)
s.strip.each_line.map { |l| " " * c + l.strip }.join("\n")
end
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
class Pkgbuild
attr_reader :filename, :contents
def initialize(filename="PKGBUILD")
@filename = filename
@contents = File.read filename
end
def =~(regex)
@contents =~ regex
end
def has_var?(name)
@contents =~ /^#{name}=/
end
def has_func?(name)
@contents =~ /^#{name}\(\)/
end
end
class PkgbuildWriter < Pkgbuild
def write
File.write(@filename, @contents)
end
def sub(*args, &block)
@contents.sub!(*args, &block)
end
def gsub(*args, &block)
@contents.gsub!(*args, &block)
end
private def replace_thing(re_gen, after, before, separator)
after.nil? ^ before.nil? or raise ArgumentError, "need either after: or before:"
return false if @contents.sub!(re_gen[]) { yield($1) }
if after.nil?
@contents.sub!(re_gen[before]) { yield("") + separator + $& }
else
@contents.sub!(re_gen[after]) { $& + separator + yield("") }
end
true
def replace_func(name, content=nil, after: nil, before: nil)
content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"
before.nil? && after.nil? and before=/[A-Za-z0-9_-]+/
re = lambda { |what=name| /^#{what}\(\)\s*{(.*?)^}/m }
replace_thing(re, after, before, "\n\n") do |orig|
"#{name}() {\n#{indent(content || yield(orig))}\n}"
def replace_var(name, content=nil, after: nil, before: nil)
content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"
re = lambda { |what=name| /^#{what}=(\([^)]*\)|.*$)/ }
replace_thing(re, after, before, "\n") do |orig|
"#{name}=#{content || yield(orig)}"
end
end
def append_array(name, content, **kwargs)
replace_var(name, **kwargs) do |orig|
if orig =~ /^\(([^)]+)\)$/
"(#{$1} #{content})"
else
"(#{content})"
end
end
end
end
class PkgbuildReader < Pkgbuild
attr_reader :pkgbase, :basevar, :makedepends, :source
def initialize(*args)
super
@bash = IO.popen ["bash"], "r+"
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
@bash.puts "{", @contents, "} &>/dev/null"
@pkgbase = read :pkgbase
@basevar = :pkgbase
if @pkgbase.empty?
@pkgbase = read :pkgname
@basevar = :pkgname
end
@makedepends = readarray :makedepends
@source = readarray :source
end
def exec(*lines)
lines.each { |l| @bash.puts l }
@bash.puts "printf '\\x00ENDEXEC\\x00'"
buf = ""
buf << @bash.readchar until buf[-1] == ?\x00 && buf =~ /\x00ENDEXEC\x00\Z/
buf[0..-10]
end
def read(varname)
exec("printf '%s' \"${#{varname}}\"")
end
def readarray(varname)
exec("printf '%s\\x00' \"${#{varname}[@]}\"").split("\x00")
end
end
class Repo
SRCDEST = "/var/lib/archbuilddest/srcdest"
DISCOVER_URLS = [
"https://gitlab.freedesktop.org/%.git",
"https://anongit.freedesktop.org/git/%",
"https://gitlab.gnome.org/GNOME/%.git",
"https://gitlab.gnome.org/%.git",
"https://github.com/%",
]
attr_reader :name, :url
def self.is_git_src?(src)
src =~ /(^|::)git(\+https?)?:\/\//
end
def self.split_src(src)
name, url = src =~ /::/ ? src.split("::", 2) : [nil, src.dup]
url.sub!(/#.*/, "")
url.sub!(/^git\+/, "")
[name, url]
end
def self.exists?(src)
_name, url = split_src src
!!Kernel.system('git', 'ls-remote', url, [:out, :err] => "/dev/null")
end
def self.discover(src)
name, url = split_src src
if url !~ /:\/\//
url = DISCOVER_URLS.map { |u| u.sub(/%/, url) }.find do |u|
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
puts "Trying #{u}"
exists?(u)
end
raise "Discover failure" if url.nil?
end
url.prepend "git+" unless Repo.is_git_src?(name)
url << "#commit=$_commit"
return "#{name}::#{url}" unless name.nil?
return url
end
def initialize(src)
@name, @url = self.class.split_src(src)
@name = url.sub(/(\.git)?\/?$/, "").sub(/^.*\//, "") if @name.nil?
@dir = File.join(SRCDEST, name)
if test ?e, @dir
system('git', 'fetch', '--all', '-p')
else
Dir.chdir(SRCDEST) do
Kernel.system('git', 'clone', '--mirror', url, name) or raise "Clone failure"
end
end
@refs = read('git', 'for-each-ref', '--sort=v:refname', '--format=%(refname)',
'refs/heads/*', 'refs/tags/*').lines.
map { |l| l.scrub.strip.sub(/^refs\/(heads|tags)\//, "") }
end
def system(*args)
Dir.chdir(@dir) do
Kernel.system(*args)
end
end
def read(*args)
ret = IO.popen(args, chdir: @dir, err: "/dev/null") { |f| f.read }
if !$?.exited?
raise "#{args[0] == "git" ? args[1] : args[0]} failed"
elsif !$?.exitstatus.zero?
raise "#{args[0] == "git" ? args[1] : args[0]} failed, exit #{$?.exitstatus}"
end
ret
end
def resolve(ref)
read('git', 'rev-parse', '--verify', '-q', "#{ref}^{commit}").chomp
end
def describe(*args)
read('git', 'describe', '--tags', *args).chomp
end
def read_file(ref, file, default=nil)
read 'git', 'show', "#{ref}:#{file}"
rescue
nil
end
def log(*args)
system('git', 'log', '--first-parent', '-m', '--decorate', *args)
end
def complete_ref(s)
@refs.grep(/^#{Regexp.escape s}/)
end
def choose_commit
header "Branches"
Jan Alexander Steffens (heftig)
committed
system 'git', '--no-pager', 'branch', '--sort=creatordate', '--column=row,dense'
Jan Alexander Steffens (heftig)
committed
system 'git', '--no-pager', 'tag', '--sort=creatordate', '--column=row,dense'
begin
old_char = Readline.completion_append_character
old_proc = Readline.completion_proc
Readline.completion_append_character = ""
Readline.completion_proc = method(:complete_ref).to_proc
commit = nil
loop do
puts
if (ref = Readline.readline("Enter ref (or nothing to view all): ", true).strip).empty?
system('tig', '--branches', '--tags', '--date-order') or log('--graph', '--branches', '--tags', '--oneline')
else
begin
commit = resolve ref
break
rescue => e
puts "Failed to resolve ref (#{e})"
end
end
end
ensure
Readline.completion_append_character = old_char
Readline.completion_proc = old_proc
end
log '--stat=80', commit
system('git', 'show', "#{commit}:NEWS") if read_file(commit, "NEWS")
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
commit
end
end
pbr = PkgbuildReader.new
reposrc = pbr.source.first
gitify = !Repo.is_git_src?(reposrc)
case ARGV.count
when 0
reposrc = Repo.discover(pbr.pkgbase) if gitify
when 1
reposrc = Repo.discover(ARGV.first)
else
raise "Invalid number of arguments"
end
repo = Repo.new reposrc
commit = repo.choose_commit
commit_comment = repo.describe('--contains', '--all', commit)
pbw = PkgbuildWriter.new
pbw.replace_var :_commit, "#{commit} # #{commit_comment}", before: :source
pbw.sub(/source=\([^) \n]+/, "source=(\"#{reposrc}\"") unless pbr.source.first == reposrc
if gitify
autogen = repo.read_file(commit, "autogen.sh") || ""
configure = repo.read_file(commit, "configure.ac") || ""
meson_build = repo.read_file(commit, "meson.build") || ""
add_md = lambda do |name, cond=true|
if !pbr.makedepends.grep(name).empty?
true
elsif cond
pbw.append_array(:makedepends, name, after: :depends)
true
else
false
end
end
add_md["git"]
meson = !meson_build.empty?
autotools = !autogen.empty?
case
when meson
add_md["meson"]
add_md["vala", meson_build =~ /'vala'/]
when autotools
unless add_md["gnome-common",
(autogen =~ /gnome-autogen\.sh/ || configure =~ /^GNOME_[A-Z_]+(\(|$)/)]
add_md["gtk-doc", configure =~ /^GTK_DOC_CHECK/]
add_md["yelp-tools", configure =~ /^YELP_HELP_INIT/]
add_md["autoconf-archive", configure =~ /^AX_/]
add_md["intltool", configure =~ /^IT_PROG_INTLTOOL/]
end
add_md["appstream-glib", configure =~ /^APPSTREAM_/]
add_md["vala", configure =~ /^AM_PROG_VALAC/]
end
if pbr.pkgbase == repo.name
localname = "$#{pbr.basevar}"
else
localname = repo.name
end
pbw.gsub(/(\${?#{pbr.basevar}}?|#{repo.name})-\${?pkgver}?/, localname)
meson &&= pbr !~ /meson/
autotools &&= pbr !~ /autogen\.sh/
{
pkgver: begin
ver_sed = ""
begin
describe = repo.describe(commit)
ver_sed << "s/^#{$&}//;" if describe =~ /^\D+/
ver_sed << "s/_/./g;" if describe =~ /_/
rescue => e
warn "pkgver() deduction failed: #{e}"
end
ver_sed << "s/-/+/g"
<<~END
cd #{localname}
git describe --tags | sed '#{ver_sed}'
END
end,
prepare: case
when meson
<<~END
cd #{localname}
END
when autotools
<<~END
cd #{localname}
NOCONFIGURE=1 ./autogen.sh
END
end,
check: case
meson test -C build --print-errorlogs
when pbr.has_func?(:check)
when autotools
<<~END
build: case
when meson
<<~END
arch-meson #{localname} build
meson compile -C build
END
end,
package: case
when meson
<<~END
meson install -C build --destdir "$pkgdir"
}.reverse_each do |name, content|
pbw.replace_func(name, content) if content
end
end
pbw.write
system "updpkgsums"
system "makepkg", "--verifysource"
diff = IO.popen(['diff', '-u', '-', pbw.filename], "r+") do |f|
f.write pbr.contents
f.close_write
f.read
end
unless diff.empty?
header "Diff"
IO.popen(["colordiff"], "w") do |f|
f.write diff
end rescue puts diff
meson_options = repo.read_file(commit, "meson_options.txt")
if meson_options
header "Meson Options"
puts meson_options, ""
end
gitmodules = repo.read_file(commit, ".gitmodules")
if gitmodules
header "Submodules"
puts gitmodules, ""
end
Dir.unlink "src"
File.unlink(*Dir.glob("*.pkg.tar*"))
# vim:set sw=2 et: