gembuild 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ # Gembuild: create Arch Linux PKGBUILDs for ruby gems.
4
+ # Copyright (C) 2015 Mario Finelli <mario@finel.li>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ module Gembuild
20
+ # Exception raised when rubygems.org returns a 404 error.
21
+ class GemNotFoundError < StandardError; end
22
+
23
+ # Exception raised when a non-string pkgbuild is passed.
24
+ class InvalidPkgbuildError < StandardError; end
25
+
26
+ # Exception raised when no gemname is specified.
27
+ class UndefinedGemNameError < StandardError; end
28
+
29
+ # Exception raised when no pkgname is specified.
30
+ class UndefinedPkgnameError < StandardError; end
31
+ end
@@ -0,0 +1,205 @@
1
+ # encoding: utf-8
2
+
3
+ # Gembuild: create Arch Linux PKGBUILDs for ruby gems.
4
+ # Copyright (C) 2015 Mario Finelli <mario@finel.li>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'mechanize'
20
+ require 'nokogiri'
21
+
22
+ module Gembuild
23
+ # This class is used to query for various information from rubygems.org.
24
+ #
25
+ # @!attribute [r] agent
26
+ # @return [Mechanize] the Mechanize agent
27
+ # @!attribute [r] deps
28
+ # @return [String] the rubygems URL for getting dependency information
29
+ # @!attribute [r] gem
30
+ # @return [String] the rubygems URL for the frontend
31
+ # @!attribute [r] gemname
32
+ # @return [String] the rubygem about which to query
33
+ # @!attribute [r] url
34
+ # @return [String] the rubygems URL to get version information
35
+ class GemScraper
36
+ attr_reader :agent, :deps, :gem, :gemname, :url
37
+
38
+ # Creates a new GemScraper instance
39
+ #
40
+ # @raise [Gembuild::UndefinedGemName] if the gemname is nil or empty
41
+ #
42
+ # @example Create a new GemScraper object
43
+ # Gembuild::GemScraper.new('mina')
44
+ # # => #<Gembuild::GemScraper:0x00000002f8a500
45
+ # # @agent=
46
+ # # #<Mechanize
47
+ # # #<Mechanize::CookieJar:0x00000002f8a410
48
+ # # @store=
49
+ # # #<HTTP::CookieJar::HashStore:0x00000002f8a370
50
+ # # @gc_index=0,
51
+ # # @gc_threshold=150,
52
+ # # @jar={},
53
+ # # @logger=nil,
54
+ # # @mon_count=0,
55
+ # # @mon_mutex=#<Mutex:0x00000002f8a320>,
56
+ # # @mon_owner=nil>>
57
+ # # nil>,
58
+ # # @deps="https://rubygems.org/api/v1/dependencies?gems=mina",
59
+ # # @gem="https://rubygems.org/gems/mina",
60
+ # # @gemname="mina",
61
+ # # @url="https://rubygems.org/api/v1/versions/mina.json">
62
+ #
63
+ # @param gemname [String] The gem about which to query.
64
+ # @return [Gembuild::GemScraper] a new GemScraper instance
65
+ def initialize(gemname)
66
+ fail Gembuild::UndefinedGemNameError if gemname.nil? || gemname.empty?
67
+
68
+ @gemname = gemname
69
+ @agent = Mechanize.new
70
+
71
+ @url = "https://rubygems.org/api/v1/versions/#{gemname}.json"
72
+ @deps = "https://rubygems.org/api/v1/dependencies?gems=#{gemname}"
73
+ @gem = "https://rubygems.org/gems/#{gemname}"
74
+ end
75
+
76
+ # Query the rubygems version api for the latest version.
77
+ #
78
+ # @raise [Gembuild::GemNotFoundError] if the page returns a 404 (not
79
+ # found) error.
80
+ #
81
+ # @example Query rubygems.org for version information
82
+ # s = Gembuild::GemScraper.new('mina')
83
+ # s.query_latest_version
84
+ # # => {:authors=>"Rico Sta. Cruz, Michael Galero",
85
+ # # :built_at=>"2015-07-08T00:00:00.000Z",
86
+ # # :created_at=>"2015-07-08T13:13:33.292Z",
87
+ # # :description=>"Really fast deployer and server automation tool.",
88
+ # # :downloads_count=>18709,
89
+ # # :metadata=>{},
90
+ # # :number=>"0.3.7",
91
+ # # :summary=>"Really fast deployer and server automation tool.",
92
+ # # :platform=>"ruby",
93
+ # # :ruby_version=>">= 0",
94
+ # # :prerelease=>false,
95
+ # # :licenses=>[],
96
+ # # :requirements=>[],
97
+ # # :sha=>
98
+ # # "bd1fa2b56ed1aded882a12f6365a04496f5cf8a14c07f8c4f1f3cfc944ef34f6"
99
+ # # }
100
+ #
101
+ # @return [Hash] the information about the latest version of the gem
102
+ def query_latest_version
103
+ response = JSON.parse(agent.get(url).body, symbolize_names: true)
104
+
105
+ # Skip any release marked as a "prerelease"
106
+ response.shift while response.first[:prerelease]
107
+
108
+ response.first
109
+ rescue Mechanize::ResponseCodeError, Net::HTTPNotFound
110
+ raise Gembuild::GemNotFoundError
111
+ end
112
+
113
+ # Gets the version number from the parsed response.
114
+ #
115
+ # @param response [Hash] The JSON parsed results from rubygems.org.
116
+ # @return [Gem::Version] the current version of the gem
117
+ def get_version_from_response(response)
118
+ Gem::Version.new(response.fetch(:number))
119
+ end
120
+
121
+ # Gets a well-formed gem description from the parsed response.
122
+ #
123
+ # @param response [Hash] The JSON parsed results from rubygems.org.
124
+ # @return [String] the gem description or summary ending in a full-stop
125
+ def format_description_from_response(response)
126
+ description = response.fetch(:description)
127
+ description = response.fetch(:summary) if description.empty?
128
+
129
+ # Replace any newlines or tabs (which would mess up a PKGBUILD) with
130
+ # spaces. Then, make sure there is no
131
+ description = description.gsub(/[[:space:]]+/, ' ').strip
132
+
133
+ # Ensure that the description ends in a full-stop.
134
+ description += '.' unless description[-1, 1] == '.'
135
+
136
+ description
137
+ end
138
+
139
+ # Gets the sha256 checksum returned from the rubygems.org API.
140
+ #
141
+ # @param response [Hash] The JSON parsed results from rubygems.org.
142
+ # @return [String] the sha256 sum of the gem file
143
+ def get_checksum_from_response(response)
144
+ response.fetch(:sha)
145
+ end
146
+
147
+ # Get the array of licenses under which the gem is licensed.
148
+ #
149
+ # @param response [Hash] The JSON parsed results from rubygems.org.
150
+ # @return [Array] the licenses for the gem
151
+ def get_licenses_from_response(response)
152
+ response.fetch(:licenses)
153
+ end
154
+
155
+ # Get all other gem dependencies for the given version.
156
+ #
157
+ # @param version [String|Gem::Version] The version for which to get the
158
+ # dependencies.
159
+ # @return [Array] list of other gems upon which the gem depends
160
+ def get_dependencies_for_version(version)
161
+ version = Gem::Version.new(version) if version.is_a?(String)
162
+
163
+ payload = Marshal.load(agent.get(deps).body)
164
+
165
+ dependencies = payload.find do |v|
166
+ Gem::Version.new(v[:number]) == version
167
+ end
168
+
169
+ dependencies[:dependencies].map(&:first)
170
+ end
171
+
172
+ # Scrape the rubygems.org frontend for the gem's homepage URL.
173
+ #
174
+ # @return [String] the homepage URL of the gem
175
+ def scrape_frontend_for_homepage_url
176
+ html = agent.get(gem).body
177
+ links = Nokogiri::HTML(html).css('a')
178
+
179
+ homepage_link = links.find do |a|
180
+ a.text.strip == 'Homepage'
181
+ end
182
+
183
+ homepage_link[:href]
184
+ end
185
+
186
+ # Quick method to get all important information in a single hash for
187
+ # later processing.
188
+ #
189
+ # @return [Hash] hash containing all the information available from the
190
+ # rubygems.org APIs and website
191
+ def scrape!
192
+ response = query_latest_version
193
+ version = get_version_from_response(response)
194
+
195
+ {
196
+ version: version,
197
+ description: format_description_from_response(response),
198
+ checksum: get_checksum_from_response(response),
199
+ license: get_licenses_from_response(response),
200
+ dependencies: get_dependencies_for_version(version),
201
+ homepage: scrape_frontend_for_homepage_url
202
+ }
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,32 @@
1
+ # Generated with gembuild (https://github.com/mfinelli/gembuild)
2
+ # Maintainer: <%= maintainer %>
3
+ <% unless contributor.count.zero? -%><% contributor.each do |c| -%>
4
+ # Contributor: <%= c %>
5
+ <% end -%><% end -%>
6
+
7
+ _gemname=<%= gemname %>
8
+ pkgname=ruby-$_gemname
9
+ pkgver=<%= pkgver %>
10
+ pkgrel=<%= pkgrel %>
11
+ <% unless epoch.zero? -%>
12
+ epoch=<%= epoch %>
13
+ <% end -%>
14
+ pkgdesc='<%= description %>'
15
+ arch=('<%= arch.join("' '") %>')
16
+ url='<%= url %>'
17
+ <% unless license.count.zero? -%>
18
+ license=('<%= license.join("' '") %>')
19
+ <% end -%>
20
+ options=(<%= options.join(' ') %>)
21
+ noextract=(<%= noextract.join(' ') %>)
22
+ depends=('<%= depends.join("' '") %>')
23
+ makedepends=('<%= makedepends.join("' '") %>')
24
+ source=("<%= source.join("\" \"") %>")
25
+ <%= checksum_type %>sums=('<%= checksum %>')
26
+
27
+ package() {
28
+ cd "$srcdir"
29
+ local _gemdir="$(ruby -e'puts Gem.default_dir')"
30
+
31
+ gem install --ignore-dependencies --no-user-install -i "$pkgdir/$_gemdir" -n "$pkgdir/usr/bin" $_gemname-$pkgver.gem
32
+ }
@@ -0,0 +1,283 @@
1
+ # encoding: utf-8
2
+
3
+ # Gembuild: create Arch Linux PKGBUILDs for ruby gems.
4
+ # Copyright (C) 2015 Mario Finelli <mario@finel.li>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'erb'
20
+
21
+ module Gembuild
22
+ # Class used to create a PKGBUILD file for a rubygem.
23
+ #
24
+ # @!attribute [rw] arch
25
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#arch
26
+ # @return [Array] the supported architectures
27
+ # @!attribute [rw] checksum
28
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#sha256sums
29
+ # @return [String] the sha256 sum of the gemfile
30
+ # @!attribute [rw] checksum_type
31
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#sha256sums
32
+ # @return [String] the type of checksum (will always be sha256)
33
+ # @!attribute [rw] contributor
34
+ # @return [Array] an array of the contributors to the pkgbuild
35
+ # @!attribute [rw] depends
36
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#depends
37
+ # @return [Array] an array of the package's dependencies (always ruby
38
+ # plus any other gems listed as dependencies)
39
+ # @!attribute [rw] description
40
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#pkgdesc
41
+ # @return [String] the package description
42
+ # @!attribute [rw] epoch
43
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#epoch
44
+ # @return [Fixnum] the package's epoch value
45
+ # @!attribute [rw] gemname
46
+ # @return [String] the ruby gem for which to generate a PKGBUILD
47
+ # @!attribute [rw] license
48
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#license
49
+ # @return [Array] an array of licenses for the gem
50
+ # @!attribute [rw] maintainer
51
+ # @return [String] the package's maintainer
52
+ # @!attribute [rw] makedepends
53
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#makedepends
54
+ # @return [Array] a list of the dependencies needed to build the package
55
+ # (normally just the package rubygems)
56
+ # @!attribute [rw] noextract
57
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#noextract
58
+ # @return [Array] a list of sources not to extract with bsdtar (namely,
59
+ # the gemfile)
60
+ # @!attribute [rw] options
61
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#options
62
+ # @return [Array] a list of options to pass to makepkg
63
+ # @!attribute [rw] pkgname
64
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#pkgname
65
+ # @return [String] the name of the package (usually ruby-gem)
66
+ # @!attribute [rw] pkgrel
67
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#pkgrel
68
+ # @return [Fixnum] the release number of the package
69
+ # @!attribute [rw] pkgver
70
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#pkgver
71
+ # @return [Gem::Version] the version of the gem
72
+ # @!attribute [rw] source
73
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#source
74
+ # @return [Array] a list of sources
75
+ # @!attribute [rw] url
76
+ # @see https://wiki.archlinux.org/index.php/PKGBUILD#url
77
+ # @return [String] the URL of the homepage of the gem
78
+ class Pkgbuild
79
+ attr_accessor :arch, :checksum, :checksum_type, :contributor, :depends,
80
+ :description, :epoch, :gemname, :license, :maintainer,
81
+ :makedepends, :noextract, :options, :pkgname, :pkgrel,
82
+ :pkgver, :source, :url
83
+
84
+ # Create a new Pkgbuild instance.
85
+ #
86
+ # @raise [Gembuild::InvalidPkgbuildError] if something other than a
87
+ # string or nil is passed as the existing pkgbuild
88
+ #
89
+ # @param gemname [String] The rubygem for which to create a PKGBUILD.
90
+ # @param existing_pkgbuild [nil, String] An old PKGBUILD that can be
91
+ # parsed for maintainer and contributor information.
92
+ # @return [Gembuild::Pkgbuild] a new Pkgbuild instance
93
+ def initialize(gemname, existing_pkgbuild = nil)
94
+ unless existing_pkgbuild.nil? || existing_pkgbuild.is_a?(String)
95
+ fail Gembuild::InvalidPkgbuildError
96
+ end
97
+
98
+ @gemname = gemname
99
+ @pkgname = "ruby-#{@gemname}"
100
+
101
+ set_package_defaults
102
+
103
+ no_parse_pkgbuild = existing_pkgbuild.nil? || existing_pkgbuild.empty?
104
+ parse_existing_pkgbuild(existing_pkgbuild) unless no_parse_pkgbuild
105
+ end
106
+
107
+ # Parse the old pkgbuild (if it exists) to get information about old
108
+ # maintainers or contributors or about other dependencies that have been
109
+ # added but that can not be scraped from rubygems.org.
110
+ #
111
+ # @param pkgbuild [String] The old PKGBUILD to parse.
112
+ # @return [Hash] a hash containing the values scraped from the PKGBUILD
113
+ def parse_existing_pkgbuild(pkgbuild)
114
+ pkgbuild.match(/^# Maintainer: (.*)$/) { |m| @maintainer = m[1] }
115
+
116
+ @contributor = pkgbuild.scan(/^# Contributor: (.*)$/).flatten
117
+
118
+ deps = parse_existing_dependencies(pkgbuild)
119
+ deps.each do |dep|
120
+ @depends << dep
121
+ end
122
+
123
+ { maintainer: maintainer, contributor: contributor, depends: deps }
124
+ end
125
+
126
+ # Create a new Pkgbuild instance with all information from the scraped
127
+ # sources assigned.
128
+ #
129
+ # @param gemname [String] The rubygem for which to create a Pkgbuild.
130
+ # @param existing_pkgbuild [String, nil] An old PKGBUILD that can be
131
+ # parsed for maintainer information.
132
+ # @return [Gembuild::Pkgbuild] a new Pkgbuild instance
133
+ def self.create(gemname, existing_pkgbuild = nil)
134
+ pkgbuild = Pkgbuild.new(gemname, existing_pkgbuild)
135
+
136
+ pkgbuild.fetch_maintainer
137
+
138
+ gem_details = Gembuild::GemScraper.new(gemname).scrape!
139
+ aur_details = Gembuild::AurScraper.new(pkgbuild.pkgname).scrape!
140
+
141
+ pkgbuild.assign_gem_details(gem_details)
142
+ pkgbuild.assign_aur_details(aur_details)
143
+
144
+ pkgbuild
145
+ end
146
+
147
+ # Generate a PKGBUILD from the class using the pkgbuild erb template.
148
+ #
149
+ # @return [String] the PKGBUILD
150
+ def render
151
+ ERB.new(template, 0, '-').result(binding)
152
+ end
153
+
154
+ # Get the PKGBUILD erb template.
155
+ #
156
+ # @return [String] the pkgbuild erb template
157
+ def template
158
+ File.read(File.join(File.dirname(__FILE__), 'pkgbuild.erb'))
159
+ end
160
+
161
+ # Write the PKGBUILD to disk.
162
+ #
163
+ # @param path [String] The directory to write the PKGBUILD.
164
+ # @return [Fixnum] the number of bytes written
165
+ def write(path = '')
166
+ File.write(File.join(File.expand_path(path), 'PKGBUILD'), render)
167
+ end
168
+
169
+ # Obfuscate the maintainer/contributors' email addresses to (help to)
170
+ # prevent spam.
171
+ #
172
+ # @param contact_information [String] The maintainer or contributor
173
+ # byline.
174
+ # @return [String] the information with the @s and .s exchanged
175
+ def format_contact_information(contact_information)
176
+ contact_information.gsub('@', ' at ').gsub('.', ' dot ')
177
+ end
178
+
179
+ # Set the correct maintainer for the PKGBUILD.
180
+ #
181
+ # If the current maintainer is nil (no old pkgbuild was passed), then do
182
+ # nothing. If there is a maintainer then compare it to the configured
183
+ # maintainer and if they are different then make the old maintainer a
184
+ # contributor before setting the correct maintainer. If the maintainer is
185
+ # nil then just set the confgured maintainer.
186
+ #
187
+ # @return [String] the pkgbuild maintainer
188
+ def fetch_maintainer
189
+ configured_maintainer = Gembuild.configure
190
+ m = "#{configured_maintainer[:name]} <#{configured_maintainer[:email]}>"
191
+ new_maintainer = format_contact_information(m)
192
+
193
+ unless maintainer.nil? || new_maintainer == maintainer
194
+ @contributor.unshift(maintainer)
195
+ end
196
+
197
+ @maintainer = new_maintainer
198
+ end
199
+
200
+ # Add the data scraped from rubygems.org to the pkgbuild.
201
+ #
202
+ # @param details [Hash] The results from GemScraper scrape.
203
+ # @return [void]
204
+ def assign_gem_details(details)
205
+ @pkgver = details.fetch(:version)
206
+ @description = details.fetch(:description)
207
+ @checksum = details.fetch(:checksum)
208
+ @license = details.fetch(:license)
209
+ @url = details.fetch(:homepage)
210
+
211
+ details.fetch(:dependencies).each do |dependency|
212
+ @depends << "ruby-#{dependency}"
213
+ end
214
+ end
215
+
216
+ # Assign version information based on the information gathered from the
217
+ # AUR.
218
+ #
219
+ # @param details [Hash, nil] The results from AurScraper scrape or nil if
220
+ # the package does not yet exist on the AUR.
221
+ # @return [void]
222
+ def assign_aur_details(details)
223
+ if details.nil?
224
+ @epoch = 0
225
+ @pkgrel = 1
226
+ else
227
+ perform_version_reconciliation(details)
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ # Set the static variables of a new pkgbuild.
234
+ #
235
+ # @return [nil]
236
+ def set_package_defaults
237
+ @checksum_type = 'sha256'
238
+ @arch = ['any']
239
+ @makedepends = ['rubygems']
240
+ @depends = ['ruby']
241
+ @source = ['https://rubygems.org/downloads/$_gemname-$pkgver.gem']
242
+ @noextract = ['$_gemname-$pkgver.gem']
243
+ @options = ['!emptydirs']
244
+ @contributor = []
245
+
246
+ nil
247
+ end
248
+
249
+ # Scrape dependencies from an existing pkgbuild.
250
+ #
251
+ # @param pkgbuild [String] The PKGBUILD to search.
252
+ # @return [Array] all existing dependencies that are not ruby or gems
253
+ def parse_existing_dependencies(pkgbuild)
254
+ match = pkgbuild.match(/^depends=\((.*?)\)$/m)[1]
255
+
256
+ # First step is to remove the leading and trailing quotes. Then convert
257
+ # all whitespace (newlines, tabs, multiple spaces, etc.) to single
258
+ # spaces. Then, make sure that strings are quoted with ' not ".
259
+ # Finally, split the packages into an array.
260
+ deps = match[1..-2].gsub(/[[:space:]]+/, ' ').tr('"', "'").split("' '")
261
+
262
+ deps.reject { |e| e.match(/^ruby/) }
263
+ rescue
264
+ []
265
+ end
266
+
267
+ # Assign the correct pkgrel and epoch depending on the current pkgver on
268
+ # the AUR and the version of the gem from rubygems.org.
269
+ #
270
+ # @param details [Hash] The results from AurScraper scrape
271
+ # @return [void]
272
+ def perform_version_reconciliation(details)
273
+ @epoch = details.fetch(:epoch)
274
+ @pkgrel = 1
275
+
276
+ if pkgver < details.fetch(:pkgver)
277
+ @epoch += 1
278
+ elsif @pkgver == details.fetch(:pkgver)
279
+ @pkgrel = details.fetch(:pkgrel) + 1
280
+ end
281
+ end
282
+ end
283
+ end