Skip to content
Snippets Groups Projects
gitpkg 8.87 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/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, derives a GNOME Git URI automatically.
    
          Otherwise, if a Git URI is provided, replaces the first source with the
          provided URI.
    
          Submodules aren't 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!
    
    def header(s)
      puts "\n\e[1m#{s}:\e[0m"
    end
    
    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
    
      def insert_func(content, where=/[A-Za-z0-9_-]+/)
        @contents.sub!(/^#{where}\(\)/, "#{content}\n\\&")
      end
    
      def replace_var(name, content=nil, after: nil, before: nil)
        content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"
        after.nil? ^ before.nil? or raise ArgumentError, "need either after: or before:"
    
        var_re = lambda { |var| /^#{var}=(\([^)]*\)|.*$)/ }
    
        if has_var? name
          @contents.sub!(var_re[name]) { "#{name}=#{content || yield($1)}" }
          return false
        end
    
        repl = "#{name}=#{content || yield("")}"
    
        if !after.nil?
          @contents.sub!(var_re[after]) { $& + "\n" + repl }
        else
          @contents.sub!(var_re[before]) { repl + "\n" + $& }
        end
    
        true
      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.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://anongit.freedesktop.org/git/",
        "https://git.gnome.org/browse/",
        "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 + url }.find do |u|
            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.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}"
    
      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"
        system 'git', 'branch'
    
        header "Tags"
        system 'git', 'tag'
    
        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")
    
    
        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
    
      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/]
      add_md["git"]
    
      if pbr.pkgbase == repo.name
        localname = "$#{pbr.basevar}"
      else
        localname = repo.name
      end
    
      pbw.gsub(/(\${?#{pbr.basevar}}?|#{repo.name})-\${?pkgver}?/, localname)
    
      if pbr !~ /autogen\.sh/
        pbw.insert_func <<~END
          prepare() {
            cd #{localname}
            NOCONFIGURE=1 ./autogen.sh
          }
        END
      end
    
      sed = ""
    
      begin
        describe = repo.describe(commit)
    
        sed << "s/^#{$&}//;" if describe =~ /^\D+/
        sed << "s/_/./g;" if describe =~ /_/
      rescue => e
        warn "pkgver() deduction failed: #{e}"
      end
    
      sed << "s/-/+/g"
    
      pbw.insert_func <<~END
        pkgver() {
          cd #{localname}
          git describe --tags | sed '#{sed}'
        }
      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"
      puts diff, ""
    end
    
    gitmodules = repo.read_file(commit, ".gitmodules")
    if gitmodules
    
      puts gitmodules
    else
      puts "None"
    end
    
    Dir.unlink "src"
    File.unlink(*Dir.glob("*.pkg.tar.xz"))