gembuild 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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