class Rake::XPI::Config

Public Class Methods

new() click to toggle source
# File lib/rake/xpi.rb, line 26
def initialize()
  @config = RecursiveOpenStruct.new(YAML::load_file('xpi.yml'), recurse_over_arrays: true)
  @timestamp = Time.now.to_i.to_s

  @config.to_h.keys.each{|key|
    self.class.send(:define_method, key) { @config[key] }
  }
end

Public Instance Methods

add_asset(tag, is_prerelease, asset_name, content, content_type) click to toggle source
# File lib/rake/xpi/publish/github.rb, line 32
def add_asset(tag, is_prerelease, asset_name, content, content_type)
  begin
    release = octokit.release_for_tag(@repo, tag.to_s)
  rescue Octokit::NotFound
    release = nil
  end
  if release
    raise "Release #{tag}: requested #{is_prerelease ? 'pre' : ''}release, found #{release.prerelease ? 'pre' : ''}release" if is_prerelease ^ release.prerelease
  else
    params = {
      name: tag.to_s,
      body: tag.to_s,
    }
    params[:target_commitish] = `git rev-list --max-parents=0 HEAD`.split(/\n/).collect{|sha| sha.strip}.first if tag.is_a?(Symbol)
    params[:prerelease] = true if is_prerelease
    release = octokit.create_release(@repo, tag.to_s, params)
  end

  # remove old version
  octokit.release_assets(release.url).each{|asset|
    next unless asset.name == asset_name
    octokit.delete_release_asset(asset.url)
  }

  octokit.upload_asset(release.url, content, name: asset_name, content_type: content_type)
end
branch() click to toggle source
# File lib/rake/xpi.rb, line 127
def branch
  if @branch.nil?
    if pull_request
      @branch = 'pull-request-' + pull_request
    elsif ENV['TRAVIS_BRANCH']
      @branch = ENV['TRAVIS_BRANCH']
    elsif ENV['CIRCLE_BRANCH']
      @branch = ENV['CIRCLE_BRANCH']
    else
      @branch = `git rev-parse --abbrev-ref HEAD`.strip
    end
  end
  return @branch
end
bump(level=nil) click to toggle source
# File lib/rake/xpi.rb, line 38
def bump(level=nil)
  level = (level || :tiny).to_sym
  level = :tiny if level == :patch
  self.release = Versionomy.parse(self.release).bump(level).to_s
end
download(url, file) click to toggle source
# File lib/rake/xpi.rb, line 112
def download(url, file)
  puts "Downloading #{url} to #{file}..."
  sh "curl -L #{url.shellescape} -o #{file.shellescape}"
end
getxpis() click to toggle source
# File lib/rake/xpi.rb, line 233
def getxpis
  return if ENV['OFFLINE'].to_s.downcase == 'true'
  return unless @config.test && @config.test.xpis && @config.test.xpis.install

  dir = File.expand_path(@config.test.xpis.install)
  FileUtils.mkdir_p(dir)
  installed = Dir["#{dir}/*.xpi"].collect{|f| File.basename(f) }

  xpis = []
  conditional = {}
  (@config.test.xpis.download || []).each{|xpi|
    if xpi.is_a?(Hash) || xpi.is_a?(RecursiveOpenStruct)
      xpi.each_pair{|env, link|
        env = env.to_s
        next unless ENV[env.upcase] == 'true' || env == 'default'
        puts "Conditional XPI: #{env.inspect} => #{link}"
        conditional[env] ||= []
        conditional[env] << link
      }
    else
      xpis << xpi
    end
  }
  conditional.delete('default') if conditional.keys.length > 1
  xpis << conditional.values 
  xpis.flatten!
  xpis = xpis.collect{|url| resolvexpi(url)}

  (installed - xpis.collect{|s| s.xpi}).each{|xpi|
    STDERR.puts "Removing #{xpi}" unless ENV['VERBOSE'] == 'false'
    File.unlink("#{dir}/#{xpi}")
  }
  xpis.reject{|s| installed.include?(s.xpi) && s.url !~ /^file:/ }.each{|s|
    # https://github.com/zotero/zotero/zipball/master for zotero master
    if s.xpi =~ /(.*)-master-.*.xpi$/
      src = $1
      tgt = "#{dir}/#{s.xpi}"
      STDERR.puts "Zipping #{s.xpi} to #{tgt}" unless ENV['VERBOSE'] == 'false'
      Dir.chdir(src){|path|
        Zip::File.open(tgt, 'w') do |zipfile|
          Dir["**/*"].sort.each{|file|
            zipfile.add(file, file)
          }
        end
      }
    else
      STDERR.puts "Downloading #{s.xpi}" unless ENV['VERBOSE'] == 'false'
      download(s.url, "#{dir}/#{s.xpi}")
    end
  }
end
id() click to toggle source
# File lib/rake/xpi.rb, line 44
def id
  return Nokogiri::XML(File.open('install.rdf')).at('//em:id').inner_text
end
octokit() click to toggle source
# File lib/rake/xpi/publish/github.rb, line 11
def octokit
  if @initialized.nil?
    raise "Github publishing not configured" unless @config.github && @config.github.user && @config.github.token
    token = ENV[@config.github.token]
    raise "Github publishing not configured" unless token

    Octokit.configure{|c|
      c.access_token = token
    }
    Octokit.middleware = Faraday::RackBuilder.new {|builder|
      builder.use Faraday::HttpCache, shared_cache: false
      builder.use Octokit::Response::RaiseError
      builder.adapter Faraday.default_adapter
    }
    Octokit.auto_paginate = true
    @initialized = true
    @repo = @config.github.user + '/' + @config.github.repo
  end
  return Octokit
end
publish() click to toggle source
# File lib/rake/xpi/publish/bintray.rb, line 20
def publish
  raise "Bintray publishing not configured" unless @config.bintray && @config.bintray.secret
  #RestClient.log = 'stdout'

  secret = ENV[@config.bintray.secret]

  client = RestClient::Resource.new('https://api.bintray.com', @config.bintray.user, secret)
  content = client['content'][@config.bintray.user][@config.bintray.repo]

  begin
    client['packages'][@config.bintray.user][@config.bintray.repo][@config.bintray.package]['versions']['latest'].delete
  rescue RestClient::ResourceNotFound
    # that's OK
  end

  content[@config.bintray.package][self.version][self.versioned_xpi].put(File.new(self.xpi), {
    content_type: 'application/x-xpinstall',
    x_bintray_package: @config.bintray.package,
    x_bintray_version: self.version,
    x_bintray_publish: '1'
  })

  if release_build?
    update_rdf("https://bintray.com/artifact/download/#{@config.bintray.user}/#{@config.bintray.repo}/#{@config.bintray.package}/#{self.version}/#{self.versioned_xpi}"){|update|
      content[@config.repo.package]['update.rdf'].put(File.new(update), {
        content_type: 'application/rdf+xml',
        x_bintray_package: @config.repo.package,
        x_bintray_version: 'latest',
        x_bintray_publish: '1',
        x_bintray_override: '1'
      })
    }
  end
end
pull_request() click to toggle source
# File lib/rake/xpi.rb, line 121
def pull_request
  return ENV['TRAVIS_PULL_REQUEST'] if (ENV['TRAVIS_PULL_REQUEST'] || 'false') != 'false'
  return ENV['CI_PULL_REQUESTS'] if (ENV['CI_PULL_REQUESTS'] || '') != ''
  return nil
end
release() click to toggle source
# File lib/rake/xpi.rb, line 47
def release
  return Nokogiri::XML(File.open('install.rdf')).at('//em:version').inner_text
end
release=(rel) click to toggle source
# File lib/rake/xpi.rb, line 50
def release=(rel)
  doc = Nokogiri::XML(File.open('install.rdf'))
  doc.at('//em:version').content = rel
  open('install.rdf', 'w'){|f| f.write(doc.to_xml)}
end
release_build?() click to toggle source
# File lib/rake/xpi.rb, line 142
def release_build?
  if @release_build.nil?
    commitmsg = `git log -n 1 --pretty=oneline`.strip
    commit = %w{CIRCLE_SHA1 TRAVIS_COMMIT}.collect{|key| ENV[key]}.compact.first
    commit ||= 'local'

    releasemsg = "#{commit} #{release_message}"

    if ENV['VERBOSE'] != 'false'
      STDERR.puts "#{self.release}"
      STDERR.puts "  committed = #{commitmsg}"
      STDERR.puts "  release   = #{releasemsg}"
      STDERR.puts "  branch    = #{branch}"
    end

    @release_build = (commitmsg == releasemsg && branch == 'master')
  end

  return @release_build
end
release_message() click to toggle source
# File lib/rake/xpi.rb, line 117
def release_message
  return "release: #{self.xpi} #{self.release}"
end
resolvexpi(source) click to toggle source
# File lib/rake/xpi.rb, line 285
def resolvexpi(source)
  STDERR.puts "Resolving #{source}" unless ENV['VERBOSE'] == 'false'

  xpi = nil

  if source =~ /\.rdf$/
    update_rdf = Nokogiri::XML(open(source))
    update_rdf.remove_namespaces!
    url = update_rdf.at('//updateLink').inner_text

  elsif source =~ /^https:\/\/addons\.mozilla\.org\//

    page = Nokogiri::HTML(open(source))
    id = page.at('//*[@data-addonid]')['data-addonid']

    info = Nokogiri::XML(open("https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/#{id}"))
    url = info.xpath('//install').detect{|install| (install['status'] || 'Release') != 'Beta'}.inner_text

  elsif source =~ /^https:\/\/github\.com\/zotero\/([^\/]+)\/zipball\/master$/
    url = source
    src = $1
    Dir.chdir(src) {
      rev = `git log -n 1 --pretty=format:"%h"`
      xpi = "#{src}-master-#{rev}.xpi"
    }
  elsif source =~ /^file:/ || source =~ /\.xpi(\?|$)/
    url = source
  else
    throw "Unsupported XPI source #{source}"
  end

  xpi ||= url.sub(/.*\//, '').sub(/\?.*$/, '')
  STDERR.puts "Resolved to #{url}" unless ENV['VERBOSE'] == 'false'
  return OpenStruct.new(url: url, xpi: xpi)
end
sign() click to toggle source
# File lib/rake/xpi.rb, line 163
def sign
  return if ENV['SIGN'] == 'false' || pull_request
  return unless @config.amo && @config.amo.issuer && @config.amo.secret
  issuer = ENV[@config.amo.issuer]
  secret = ENV[@config.amo.secret]
  return unless issuer && secret

  token = lambda {
    payload = {
      jti: SecureRandom.base64,
      iss: issuer,
      iat: Time.now.utc.to_i,
      exp: Time.now.utc.to_i + 60,
    }
    return JWT.encode(payload, secret, 'HS256')
  }

  duration = lambda{|secs|
    secs = secs.to_i
    mins  = secs / 60
    hours = mins / 60
    days  = hours / 24

    if days > 0
      "#{days} days and #{hours % 24} hours"
    elsif hours > 0
      "#{hours} hours and #{mins % 60} minutes"
    elsif mins > 0
      "#{mins} minutes and #{secs % 60} seconds"
    elsif secs >= 0
      "#{secs} seconds"
    end
  }

  url = "https://addons.mozilla.org/api/v3/addons/#{self.id}/versions/#{self.version}/"

  wait = nil
  begin
    Dir.mktmpdir{|dir|
      tmp = File.join(dir, self.versioned_xpi)
      FileUtils.cp(self.xpi, tmp)
      puts "Submit #{tmp} to #{url} for signing"
      RestClient.put(url, {upload: File.new(tmp)}, { 'Authorization' => "JWT #{token.call}", 'Content-Type' => 'multipart/form-data' })
    }
    wait = Time.now.to_i
    sleep 10
  rescue RestClient::Conflict
    puts "#{self.xpi} already signed"
    wait = Time.now.to_i
  end

  status = {}
  (1..100).each{|attempt|
    status = JSON.parse(RestClient.get(url, { 'Authorization' => "JWT #{token.call}"} ).to_s)
    files = (status['files'] || []).length
    signed = (files > 0 ? status['files'][0]['signed'] : false)
    puts "attempt #{attempt} after #{duration.call(Time.now.to_i - wait)}, #{files} files, signed: #{signed}"
    break if signed
    sleep 5
  }

  raise "Unexpected response: #{status['files'].inspect}" if !status['files'] || status['files'].length != 1 || !status['files'][0]['download_url']
  raise "Not signed: #{status['files'][0].inspect}" unless status['files'][0]['signed']

  puts "\ngetting signed XPI from #{status['files'][0]['download_url']}"
  File.open(self.xpi, 'wb'){|f|
    f.write(RestClient.get(status['files'][0]['download_url'], { 'Authorization' => "JWT #{token.call}"} ).body)
  }
end
update_rdf() { |path| ... } click to toggle source
# File lib/rake/xpi.rb, line 70
def update_rdf
  _id = self.id
  _release = self.release
  _changelog = self.changelog
  _link = self.xpi_url

  update = Nokogiri::XML::Builder.new { |xml|
    xml.RDF('xmlns:RDF'=>'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'xmlns:em' =>
    'http://www.mozilla.org/2004/em-rdf#') {
      xml.parent.namespace = xml.parent.namespace_definitions.find{|ns|ns.prefix=='RDF'}
      xml['RDF'].Description(about: "urn:mozilla:extension:#{_id}") {
        xml['em'].updates {
          xml['RDF'].Seq {
            xml['RDF'].li {
              xml['RDF'].Description {
                xml['em'].version { xml.text _release }

                Nokogiri::XML(open('install.rdf')).xpath('//em:targetApplication/xmlns:Description').each{|target|
                  xml['em'].targetApplication {
                    xml['RDF'].Description {
                      xml['em'].id { xml.text target.at('./em:id').inner_text }
                      xml['em'].minVersion { xml.text target.at('./em:minVersion').inner_text }
                      xml['em'].maxVersion { xml.text target.at('./em:maxVersion').inner_text }
                      xml['em'].updateLink { xml.text _link }
                      xml['em'].updateInfoURL { xml.text _changelog }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  Tempfile.create('update_rdf') do |tmp|
    tmp.write(update.to_xml)
    tmp.close
    yield tmp.path
  end
end
update_url() click to toggle source
# File lib/rake/xpi/publish/github.rb, line 59
def update_url
  return "https://github.com/#{@config.github.user}/#{@config.github.repo}/releases/download/update.rdf/update.rdf"
end
version() click to toggle source
# File lib/rake/xpi.rb, line 56
def version
  if release_build?
    return release
  elsif ENV['TRAVIS_BUILD_NUMBER']
    return release + "-#{branch}-#{ENV['TRAVIS_BUILD_NUMBER']}"
  elsif ENV['CIRCLE_BUILD_NUM']
    br = branch
    br = "br#{br}" unless br =~ /^[a-z]/i
    return release + "-#{br}-#{ENV['CIRCLE_BUILD_NUM']}"
  else
    return release + "-#{Socket.gethostname}-#{@timestamp}"
  end
end
versioned_xpi(v=nil) click to toggle source
# File lib/rake/xpi.rb, line 34
def versioned_xpi(v=nil)
  return File.basename(self.xpi, File.extname(self.xpi)) + '-' + (v || self.version) + File.extname(self.xpi)
end
xpi_url() click to toggle source
# File lib/rake/xpi/publish/github.rb, line 62
def xpi_url
  return "https://github.com/#{@config.github.user}/#{@config.github.repo}/releases/download/#{release_build? ? self.version : @@builds}/#{self.versioned_xpi}"
end