Skip to content
Snippets Groups Projects
gitpkg 10.7 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, 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
    
    
    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.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!(/\?signed/, "")
    
        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|
    
            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}"
    
      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', '--no-pager', 'branch', '--sort=creatordate', '--column=row,dense'
    
        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")
    
    
        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/
    
          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
    
          <<~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
    
            make -C #{localname} check
    
            meson compile -C build
    
            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, ""
    
    File.unlink(*Dir.glob("*.pkg.tar*"))