gembrew 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46aaf0e8b85829162de73512e6c9fe6c892a7dc646513a6cd6d6d9265c06124b
4
+ data.tar.gz: 90de5846e5cc45f8e0458d0c0dcf6368ea4071595b152b72279cff91b4e349f1
5
+ SHA512:
6
+ metadata.gz: 40a61361c893f96bc402ef4c4514236cb7ec4313386f3378e1dfcf026a4726a2fbfec786f6ccdd8120495d7c80adfe240f3b182e79d3890e5b2e6bfa2bff57ca
7
+ data.tar.gz: edf9bd6364b9c2409b43d1777739b9591d02adaee91475b72203b64dbfdd5c75f566aab6d632aa1998318f745120ccfb5102ad7f937b6f9be323817521e575e4
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Danny Ben Shitrit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Gembrew
2
+
3
+ Generate conventional Homebrew formulae for published Ruby command-line gems.
4
+
5
+ ## Install
6
+
7
+ ```shell
8
+ gem install gembrew
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```shell
14
+ mkdir homebrew-tap
15
+ cd homebrew-tap
16
+ gembrew init GEM
17
+ gembrew build
18
+ gembrew check
19
+ ```
20
+
21
+ `init` requires an empty directory, with the exception of Git's `.git`
22
+ directory. It creates a tap repository containing `gembrew.yml`, `Formula/`,
23
+ and a Docker Compose environment under `support/`. `build` reads the
24
+ configuration and writes the formula.
25
+
26
+ The configuration filename is always `gembrew.yml`.
27
+
28
+ For example, `gembrew init example` generates:
29
+
30
+ ```yaml
31
+ gem: example
32
+ output: Formula/example.rb
33
+
34
+ test: |-
35
+ system bin/"example", "--version"
36
+ ```
37
+
38
+ Edit `gembrew.yml` as needed, then generate the formula:
39
+
40
+ ```shell
41
+ gembrew build
42
+ ```
43
+
44
+ The required settings are `gem`, `output`, and exactly one of `test` or
45
+ `test_from_file`. Relative paths are resolved from `gembrew.yml`.
46
+
47
+ ```yaml
48
+ gem: example
49
+ output: Formula/example.rb
50
+
51
+ # Optional published gem version. The latest stable version is used by default.
52
+ version: "1.2.3"
53
+
54
+ # Optional gem metadata overrides:
55
+ desc: Example command-line application
56
+ homepage: https://example.com
57
+ license: MIT
58
+ executable: example
59
+
60
+ test: |-
61
+ system bin/"example", "--version"
62
+
63
+ # Use this instead of `test` to load the test body from another file:
64
+ # test_from_file: support/test.rb
65
+ ```
66
+
67
+ Gembrew resolves the generic Ruby dependency graph and generates a Homebrew
68
+ `resource` for every runtime dependency. Original `.gem` archives are reused
69
+ from RubyGems' local cache when available. Downloaded archives are retained in
70
+ `${XDG_CACHE_HOME:-~/.cache}/gembrew/gems`.
71
+
72
+ Open the generated Homebrew environment with:
73
+
74
+ ```shell
75
+ gembrew shell
76
+ ```
77
+
78
+ The repository is mounted as a local tap, so formulae can be addressed by gem
79
+ name inside the shell:
80
+
81
+ ```shell
82
+ brew style example
83
+ brew audit --new --online example
84
+ brew install --build-from-source example
85
+ brew test example
86
+ ```
87
+
88
+ Run the complete workflow non-interactively in one clean container:
89
+
90
+ ```shell
91
+ gembrew check
92
+ ```
93
+
94
+ This rebuilds the formula, then runs Homebrew style, online audit, source
95
+ installation, and the formula test. The generated Compose `check` service can
96
+ also be invoked directly.
97
+
98
+ ## Contributing / Support
99
+
100
+ If you experience any issue, have a question or a suggestion, or if you wish
101
+ to contribute, feel free to [open an issue][issues].
102
+
103
+ ---
104
+
105
+ [issues]: https://github.com/DannyBen/gembrew/issues
data/bin/gembrew ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'gembrew'
4
+ require 'gembrew/cli'
5
+ require 'colsole'
6
+
7
+ include Colsole
8
+
9
+ runner = Gembrew::CLI.runner
10
+
11
+ begin
12
+ exit runner.run ARGV
13
+ rescue => e
14
+ puts e.backtrace.reverse if ENV['DEBUG']
15
+ say! "rib` #{e.class} `\n#{e.message}"
16
+ exit 1
17
+ end
@@ -0,0 +1,91 @@
1
+ require 'fileutils'
2
+ require 'open3'
3
+ require 'pathname'
4
+ require 'rubygems/package'
5
+ require 'gembrew/error'
6
+ require 'gembrew/reporter'
7
+
8
+ module Gembrew
9
+ class ArchiveStore
10
+ attr_reader :cache_path
11
+
12
+ def initialize(cache_path:, reporter: Reporter.new, command_runner: nil)
13
+ @cache_path = Pathname(cache_path)
14
+ @reporter = reporter
15
+ @command_runner = command_runner || method(:run_command)
16
+ @cache_path.mkpath
17
+ end
18
+
19
+ def fetch(name, version, download_path, index: nil, total: nil)
20
+ if (path = locally_cached(name, version))
21
+ report :rubygems_cache, name, version, index: index, total: total
22
+ return path
23
+ end
24
+
25
+ if version && (path = gembrew_cached(name, version))
26
+ report :gembrew_cache, name, version, index: index, total: total
27
+ return path
28
+ end
29
+
30
+ download name, version, Pathname(download_path), index: index, total: total
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :reporter, :command_runner
36
+
37
+ def locally_cached(name, version)
38
+ return unless version
39
+
40
+ Gem.path.each do |gem_path|
41
+ path = Pathname(gem_path)/'cache'/"#{name}-#{version}.gem"
42
+ return path if valid_archive?(path, name, version)
43
+ end
44
+
45
+ nil
46
+ end
47
+
48
+ def gembrew_cached(name, version)
49
+ path = cache_path/"#{name}-#{version}.gem"
50
+ path if valid_archive?(path, name, version)
51
+ end
52
+
53
+ def valid_archive?(path, name, version)
54
+ return false unless path.file?
55
+
56
+ spec = Gem::Package.new(path.to_s).spec
57
+ spec.name == name && spec.version == Gem::Version.new(version.to_s) &&
58
+ spec.platform == Gem::Platform::RUBY
59
+ rescue Gem::Package::Error
60
+ false
61
+ end
62
+
63
+ def download(name, version, download_path, index:, total:)
64
+ report :downloading, name, version, index: index, total: total
65
+ before = download_path.children
66
+ command = ['gem', 'fetch', name, '--platform', 'ruby']
67
+ command.push '--version', version.to_s if version
68
+ command_runner.call(*command, chdir: download_path)
69
+
70
+ candidates = download_path.children - before
71
+ archive = candidates.find { |path| path.extname == '.gem' }
72
+ raise Error, "gem fetch did not produce a .gem file for #{name} #{version}" unless archive
73
+
74
+ spec = Gem::Package.new(archive.to_s).spec
75
+ cached_path = cache_path/"#{spec.name}-#{spec.version}.gem"
76
+ FileUtils.mv archive, cached_path, force: true
77
+ cached_path
78
+ end
79
+
80
+ def report(state, name, version, index:, total:)
81
+ reporter.archive state, name, version, index: index, total: total
82
+ end
83
+
84
+ def run_command(*command, chdir:)
85
+ result, status = Open3.capture2e(*command, chdir: chdir.to_s)
86
+ raise Error, result unless status.success?
87
+
88
+ result
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ require 'mister_bin'
2
+ require 'gembrew/commands/build'
3
+ require 'gembrew/commands/check'
4
+ require 'gembrew/commands/init'
5
+ require 'gembrew/commands/shell'
6
+ require 'gembrew/version'
7
+
8
+ module Gembrew
9
+ class CLI
10
+ def self.runner
11
+ runner = MisterBin::Runner.new version: Gembrew::VERSION,
12
+ header: 'Gembrew - Homebrew Formula Generator for Ruby Gems',
13
+ footer: 'Run m`gembrew COMMAND --help` for more information'
14
+
15
+ runner.route 'init', to: Commands::Init
16
+ runner.route 'build', to: Commands::Build
17
+ runner.route 'check', to: Commands::Check
18
+ runner.route 'shell', to: Commands::Shell
19
+
20
+ runner
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ require 'mister_bin'
2
+ require 'gembrew/generator'
3
+
4
+ module Gembrew
5
+ module Commands
6
+ class Build < MisterBin::Command
7
+ help 'Generate a Homebrew formula from gembrew.yml'
8
+
9
+ usage 'gembrew build'
10
+ usage 'gembrew build (-h|--help)'
11
+
12
+ def run
13
+ output_path = Generator.new.call
14
+ say "Generated m`#{output_path}`"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require 'mister_bin'
2
+ require 'gembrew/docker_check'
3
+ require 'gembrew/generator'
4
+
5
+ module Gembrew
6
+ module Commands
7
+ class Check < MisterBin::Command
8
+ help 'Build and test the formula in a clean Homebrew environment'
9
+
10
+ usage 'gembrew check'
11
+ usage 'gembrew check (-h|--help)'
12
+
13
+ def run
14
+ output_path = Generator.new.call
15
+ say "Generated m`#{output_path}`"
16
+ DockerCheck.new.call
17
+ say 'g`All checks passed`'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ require 'mister_bin'
2
+ require 'gembrew/initializer'
3
+
4
+ module Gembrew
5
+ module Commands
6
+ class Init < MisterBin::Command
7
+ help 'Initialize a Homebrew tap repository for a Ruby gem'
8
+
9
+ usage 'gembrew init GEM'
10
+ usage 'gembrew init (-h|--help)'
11
+
12
+ param 'GEM', 'Published Ruby gem name'
13
+
14
+ def run
15
+ Initializer.new(args['GEM']).call
16
+ say "Initialized m`#{Dir.pwd}` for bb`#{args['GEM']}`"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require 'mister_bin'
2
+ require 'gembrew/docker_shell'
3
+
4
+ module Gembrew
5
+ module Commands
6
+ class Shell < MisterBin::Command
7
+ help 'Open a Homebrew shell for this tap repository'
8
+
9
+ usage 'gembrew shell'
10
+ usage 'gembrew shell (-h|--help)'
11
+
12
+ def run
13
+ DockerShell.new.call
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,70 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'gembrew/error'
4
+
5
+ module Gembrew
6
+ class Config
7
+ FILENAME = 'gembrew.yml'
8
+ ALLOWED_KEYS = %w[
9
+ gem version output desc homepage license executable test test_from_file
10
+ ].freeze
11
+
12
+ attr_reader :path
13
+
14
+ def initialize(project_path = Pathname.pwd)
15
+ @path = Pathname(project_path).expand_path/FILENAME
16
+ @data = load_data
17
+ validate
18
+ end
19
+
20
+ def gem_name = data.fetch('gem').to_s
21
+ def version = data['version']&.to_s
22
+ def output_path = (path.dirname/data.fetch('output')).expand_path
23
+ def description = data['desc']
24
+ def homepage = data['homepage']
25
+ def license = data['license']
26
+ def executable = data['executable']
27
+
28
+ def test_body
29
+ @test_body ||= if data.has_key?('test')
30
+ data.fetch('test').to_s
31
+ else
32
+ test_path = path.dirname/data.fetch('test_from_file')
33
+ raise Error, "Test file does not exist: #{test_path}" unless test_path.file?
34
+
35
+ test_path.read
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :data
42
+
43
+ def load_data
44
+ raise Error, "Configuration does not exist: #{path}" unless path.file?
45
+
46
+ YAML.safe_load_file(path, aliases: false)
47
+ rescue Psych::Exception => e
48
+ raise Error, "Invalid configuration: #{e.message}"
49
+ end
50
+
51
+ def validate
52
+ raise Error, 'Configuration must be a YAML mapping' unless data.is_a?(Hash)
53
+
54
+ unknown_keys = data.keys - ALLOWED_KEYS
55
+ raise Error, "Unknown configuration keys: #{unknown_keys.join(', ')}" if unknown_keys.any?
56
+
57
+ require_value 'gem'
58
+ require_value 'output'
59
+
60
+ return unless data.has_key?('test') == data.has_key?('test_from_file')
61
+
62
+ raise Error, 'Provide exactly one of test or test_from_file'
63
+ end
64
+
65
+ def require_value(key)
66
+ value = data[key]
67
+ raise Error, "#{key} is required" if value.nil? || value.to_s.empty?
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ require 'pathname'
2
+ require 'gembrew/error'
3
+
4
+ module Gembrew
5
+ class DockerCheck
6
+ COMPOSE_PATH = Pathname('support/compose.yaml')
7
+
8
+ attr_reader :project_path
9
+
10
+ def initialize(project_path: Pathname.pwd, command_runner: nil)
11
+ @project_path = Pathname(project_path).expand_path
12
+ @command_runner = command_runner || method(:run_command)
13
+ end
14
+
15
+ def call
16
+ raise Error, "Compose file does not exist: #{compose_path}" unless compose_path.file?
17
+
18
+ success = command_runner.call(
19
+ 'docker', 'compose', '-f', COMPOSE_PATH.to_s,
20
+ 'run', '--rm', '--no-TTY', 'check',
21
+ chdir: project_path
22
+ )
23
+ raise Error, 'Homebrew checks failed' unless success
24
+
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :command_runner
31
+
32
+ def compose_path
33
+ project_path/COMPOSE_PATH
34
+ end
35
+
36
+ def run_command(*command, chdir:)
37
+ system(*command, chdir: chdir.to_s)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ require 'pathname'
2
+ require 'gembrew/error'
3
+
4
+ module Gembrew
5
+ class DockerShell
6
+ COMPOSE_PATH = Pathname('support/compose.yaml')
7
+
8
+ attr_reader :project_path
9
+
10
+ def initialize(project_path: Pathname.pwd, command_runner: nil)
11
+ @project_path = Pathname(project_path).expand_path
12
+ @command_runner = command_runner || method(:run_command)
13
+ end
14
+
15
+ def call
16
+ raise Error, "Compose file does not exist: #{compose_path}" unless compose_path.file?
17
+
18
+ success = command_runner.call(
19
+ 'docker', 'compose', '-f', COMPOSE_PATH.to_s, 'run', '--rm', 'brew',
20
+ chdir: project_path
21
+ )
22
+ raise Error, 'Homebrew shell exited with an error' unless success
23
+
24
+ true
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :command_runner
30
+
31
+ def compose_path
32
+ project_path/COMPOSE_PATH
33
+ end
34
+
35
+ def run_command(*command, chdir:)
36
+ system(*command, chdir: chdir.to_s)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Gembrew
2
+ class Error < StandardError; end
3
+ end
@@ -0,0 +1,54 @@
1
+ require 'gembrew/config'
2
+ require 'gembrew/renderer'
3
+ require 'gembrew/resolver'
4
+
5
+ module Gembrew
6
+ class Generator
7
+ def initialize(config: Config.new, resolver: Resolver.new, renderer: Renderer.new)
8
+ @config = config
9
+ @resolver = resolver
10
+ @renderer = renderer
11
+ end
12
+
13
+ def call
14
+ resolution = resolver.resolve(config.gem_name, config.version)
15
+ output_path = config.output_path
16
+ output_path.dirname.mkpath
17
+ output_path.write renderer.render(formula_data(resolution))
18
+ output_path
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :config, :resolver, :renderer
24
+
25
+ def formula_data(resolution)
26
+ spec = resolution.spec
27
+ {
28
+ formula_name: formula_name(spec.name),
29
+ gem_name: spec.name,
30
+ version: spec.version.to_s,
31
+ sha256: resolution.sha256,
32
+ resources: resolution.resources,
33
+ }.merge metadata(spec)
34
+ end
35
+
36
+ def metadata(spec)
37
+ {
38
+ description: config.description || spec.summary,
39
+ homepage: config.homepage || inferred_homepage(spec),
40
+ license: config.license || spec.license || spec.licenses.first,
41
+ executable: config.executable || spec.executables.first || spec.name,
42
+ test_body: config.test_body,
43
+ }
44
+ end
45
+
46
+ def formula_name(gem_name)
47
+ gem_name.split(/[-_]/).map(&:capitalize).join
48
+ end
49
+
50
+ def inferred_homepage(spec)
51
+ spec.homepage.to_s.empty? ? spec.metadata['homepage_uri'] : spec.homepage
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,63 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'gembrew/error'
5
+
6
+ module Gembrew
7
+ class Initializer
8
+ CONFIG_FILENAME = 'gembrew.yml'
9
+
10
+ attr_reader :gem_name, :project_path
11
+
12
+ def initialize(gem_name, project_path: Pathname.pwd)
13
+ @gem_name = gem_name
14
+ @project_path = Pathname(project_path).expand_path
15
+ end
16
+
17
+ def call
18
+ raise Error, "Directory is not empty: #{project_path} (found: #{entry_names.join(', ')})" if entries.any?
19
+
20
+ formula_path.mkpath
21
+ support_path.mkpath
22
+ config_path.write render
23
+ compose_path.write render('compose.yaml.erb')
24
+
25
+ project_path
26
+ end
27
+
28
+ private
29
+
30
+ def config_path
31
+ project_path/CONFIG_FILENAME
32
+ end
33
+
34
+ def compose_path
35
+ support_path/'compose.yaml'
36
+ end
37
+
38
+ def formula_path
39
+ project_path/'Formula'
40
+ end
41
+
42
+ def support_path
43
+ project_path/'support'
44
+ end
45
+
46
+ def entries
47
+ @entries ||= project_path.children.reject { |path| path.basename.to_s == '.git' }
48
+ end
49
+
50
+ def entry_names
51
+ entries.map { |path| path.basename.to_s }.sort
52
+ end
53
+
54
+ def render(filename = 'gembrew.yml.erb')
55
+ template = asset_path filename
56
+ ERB.new(template.read, trim_mode: '-').result(binding)
57
+ end
58
+
59
+ def asset_path(filename)
60
+ Pathname(__dir__)/'templates'/filename
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ require 'erb'
2
+ require 'pathname'
3
+
4
+ module Gembrew
5
+ class Renderer
6
+ def render(locals)
7
+ context = Object.new
8
+ locals.each { |name, value| context.define_singleton_method(name) { value } }
9
+ ERB.new(template_path.read, trim_mode: '-').result(context.instance_eval { binding })
10
+ end
11
+
12
+ private
13
+
14
+ def template_path
15
+ Pathname(__dir__)/'templates/formula.rb.erb'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ require 'colsole'
2
+
3
+ module Gembrew
4
+ class Reporter
5
+ include Colsole
6
+
7
+ LABELS = {
8
+ rubygems_cache: ['g', 'RubyGems cache'],
9
+ gembrew_cache: ['g', 'Gembrew cache'],
10
+ downloading: %w[y Downloading],
11
+ }.freeze
12
+
13
+ def archive(state, name, version, index: nil, total: nil)
14
+ color, label = LABELS.fetch state
15
+ parts = []
16
+ parts << counter(index, total) if index && total
17
+ parts << "#{color}`#{label.ljust label_width}`"
18
+ parts << ["nb`#{name}`", version&.to_s].compact.join(' ')
19
+ say parts.join(' ')
20
+ end
21
+
22
+ def resolving(name, version)
23
+ say "Resolving dependencies for nb`#{name}` #{version}..."
24
+ end
25
+
26
+ def collecting(count)
27
+ say "Collecting nb`#{count}` resources..."
28
+ end
29
+
30
+ private
31
+
32
+ def counter(index, total)
33
+ "%#{total.to_s.length}d/%d" % [index, total]
34
+ end
35
+
36
+ def label_width
37
+ @label_width ||= LABELS.values.map { |_color, label| label.length }.max
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,99 @@
1
+ require 'bundler'
2
+ require 'digest'
3
+ require 'open3'
4
+ require 'pathname'
5
+ require 'rubygems/package'
6
+ require 'tmpdir'
7
+ require 'gembrew/archive_store'
8
+ require 'gembrew/error'
9
+ require 'gembrew/reporter'
10
+
11
+ module Gembrew
12
+ Resource = Data.define(:name, :version, :sha256)
13
+ Resolution = Data.define(:spec, :sha256, :resources)
14
+
15
+ class Resolver
16
+ attr_reader :cache_root
17
+
18
+ def initialize(cache_root: nil, reporter: Reporter.new, command_runner: nil, archive_store: nil)
19
+ @cache_root = Pathname(cache_root || default_cache_root)
20
+ @reporter = reporter
21
+ @command_runner = command_runner || method(:run_command)
22
+ @archive_store = archive_store || ArchiveStore.new(
23
+ cache_path: @cache_root/'gems', reporter: reporter, command_runner: @command_runner
24
+ )
25
+ temporary_root.mkpath
26
+ end
27
+
28
+ def resolve(name, version = nil)
29
+ Dir.mktmpdir('build-', temporary_root.to_s) do |directory|
30
+ temporary = Pathname(directory)
31
+ root_archive = archive_store.fetch(name, version, temporary)
32
+ root_spec = Gem::Package.new(root_archive.to_s).spec
33
+ dependencies = dependency_specs(lock(root_spec, temporary), root_spec)
34
+
35
+ reporter.collecting dependencies.length
36
+ resources = dependencies.each_with_index.map do |spec, index|
37
+ resource spec, temporary, index + 1, dependencies.length
38
+ end
39
+
40
+ Resolution.new(root_spec, Digest::SHA256.file(root_archive).hexdigest, resources)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :reporter, :command_runner, :archive_store
47
+
48
+ def resource(spec, temporary, index, total)
49
+ archive = archive_store.fetch spec.name, spec.version, temporary, index: index, total: total
50
+ Resource.new spec.name, spec.version.to_s, Digest::SHA256.file(archive).hexdigest
51
+ end
52
+
53
+ def default_cache_root
54
+ cache_home = ENV.fetch('XDG_CACHE_HOME', File.expand_path('~/.cache'))
55
+ Pathname(cache_home)/'gembrew'
56
+ end
57
+
58
+ def temporary_root
59
+ cache_root/'tmp'
60
+ end
61
+
62
+ def lock(root_spec, temporary)
63
+ gemfile = temporary/'Gemfile'
64
+ lockfile = temporary/'Gemfile.lock'
65
+ gemfile.write <<~RUBY
66
+ source "https://rubygems.org"
67
+ gem #{root_spec.name.inspect}, "= #{root_spec.version}"
68
+ RUBY
69
+
70
+ reporter.resolving root_spec.name, root_spec.version
71
+ command_runner.call(
72
+ 'bundle', 'lock', '--gemfile', gemfile.to_s, '--lockfile', lockfile.to_s,
73
+ '--add-platform', 'ruby',
74
+ chdir: temporary,
75
+ env: {
76
+ 'BUNDLE_FORCE_RUBY_PLATFORM' => 'true',
77
+ 'BUNDLE_GEMFILE' => gemfile.to_s,
78
+ }
79
+ )
80
+ lockfile
81
+ end
82
+
83
+ def dependency_specs(lockfile, root_spec)
84
+ specs = Bundler::LockfileParser.new(lockfile.read).specs
85
+ selected = specs.group_by(&:name).values.map do |variants|
86
+ variants.find { |spec| spec.platform == Gem::Platform::RUBY } || variants.first
87
+ end
88
+
89
+ selected.reject { |spec| spec.name == root_spec.name }.sort_by(&:name)
90
+ end
91
+
92
+ def run_command(*command, chdir:, env: {})
93
+ result, status = Open3.capture2e(env, *command, chdir: chdir.to_s)
94
+ raise Error, result unless status.success?
95
+
96
+ result
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ services:
2
+ brew: &base
3
+ image: homebrew/brew:main
4
+ working_dir: /work
5
+ environment:
6
+ HOMEBREW_NO_AUTO_UPDATE: "1"
7
+ HOMEBREW_NO_INSTALL_CLEANUP: "1"
8
+ HOMEBREW_NO_INSTALL_FROM_API: "1"
9
+ GEMBREW_GEM: <%= gem_name %>
10
+ PS1: "\n\nbrew $ "
11
+ volumes:
12
+ - ..:/work
13
+ - ..:/home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/gembrew/homebrew-tap
14
+ command: ["bash", "--norc"]
15
+ stdin_open: true
16
+ tty: true
17
+
18
+ check:
19
+ <<: *base
20
+ command:
21
+ - bash
22
+ - -euc
23
+ - |
24
+ brew style "$${GEMBREW_GEM}"
25
+ brew audit --new --online "$${GEMBREW_GEM}"
26
+ brew install --build-from-source "$${GEMBREW_GEM}"
27
+ brew test "$${GEMBREW_GEM}"
28
+ stdin_open: false
29
+ tty: false
@@ -0,0 +1,39 @@
1
+ # Generated by Gembrew: https://github.com/dannyben/gembrew
2
+ # Manual edits are not recommended. Update gembrew.yml and regenerate with:
3
+ # gem install gembrew
4
+ # gembrew build
5
+
6
+ class <%= formula_name %> < Formula
7
+ desc <%= description.inspect %>
8
+ homepage <%= homepage.inspect %>
9
+ url "https://rubygems.org/downloads/<%= gem_name %>-<%= version %>.gem"
10
+ sha256 "<%= sha256 %>"
11
+ license <%= license.inspect %>
12
+
13
+ depends_on "ruby"
14
+ <% resources.each do |resource| -%>
15
+
16
+ resource <%= resource.name.inspect %> do
17
+ url "https://rubygems.org/downloads/<%= resource.name %>-<%= resource.version %>.gem"
18
+ sha256 "<%= resource.sha256 %>"
19
+ end
20
+ <% end -%>
21
+
22
+ def install
23
+ ENV["GEM_HOME"] = libexec
24
+
25
+ resources.each do |resource|
26
+ system "gem", "install", resource.cached_download,
27
+ "--ignore-dependencies", "--install-dir", libexec, "--no-document"
28
+ end
29
+
30
+ system "gem", "install", cached_download,
31
+ "--ignore-dependencies", "--install-dir", libexec, "--no-document"
32
+
33
+ (bin/<%= executable.inspect %>).write_env_script libexec/"bin/<%= executable %>", GEM_HOME: ENV.fetch("GEM_HOME")
34
+ end
35
+
36
+ test do
37
+ <%= test_body.lines.map { |line| " #{line}" }.join %>
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ gem: <%= gem_name %>
2
+ output: Formula/<%= gem_name %>.rb
3
+
4
+ test: |-
5
+ system bin/"<%= gem_name %>", "--version"
6
+
7
+ # Optional overrides:
8
+ # version: "1.2.3"
9
+ # desc: Example command-line application
10
+ # homepage: https://example.com
11
+ # license: MIT
12
+ # executable: <%= gem_name %>
13
+
14
+ # To load the Homebrew test body from a Ruby file, replace `test` above with:
15
+ # test_from_file: support/test.rb
@@ -0,0 +1,3 @@
1
+ module Gembrew
2
+ VERSION = '0.1.0'
3
+ end
data/lib/gembrew.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'gembrew/archive_store'
2
+ require 'gembrew/config'
3
+ require 'gembrew/docker_check'
4
+ require 'gembrew/docker_shell'
5
+ require 'gembrew/error'
6
+ require 'gembrew/generator'
7
+ require 'gembrew/initializer'
8
+ require 'gembrew/reporter'
9
+ require 'gembrew/renderer'
10
+ require 'gembrew/resolver'
11
+ require 'gembrew/version'
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gembrew
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Danny Ben Shitrit
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: colsole
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mister_bin
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.9'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
54
+ description: Generate conventional Homebrew formulae for published Ruby command-line
55
+ gems
56
+ email: db@dannyben.com
57
+ executables:
58
+ - gembrew
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - bin/gembrew
65
+ - lib/gembrew.rb
66
+ - lib/gembrew/archive_store.rb
67
+ - lib/gembrew/cli.rb
68
+ - lib/gembrew/commands/build.rb
69
+ - lib/gembrew/commands/check.rb
70
+ - lib/gembrew/commands/init.rb
71
+ - lib/gembrew/commands/shell.rb
72
+ - lib/gembrew/config.rb
73
+ - lib/gembrew/docker_check.rb
74
+ - lib/gembrew/docker_shell.rb
75
+ - lib/gembrew/error.rb
76
+ - lib/gembrew/generator.rb
77
+ - lib/gembrew/initializer.rb
78
+ - lib/gembrew/renderer.rb
79
+ - lib/gembrew/reporter.rb
80
+ - lib/gembrew/resolver.rb
81
+ - lib/gembrew/templates/compose.yaml.erb
82
+ - lib/gembrew/templates/formula.rb.erb
83
+ - lib/gembrew/templates/gembrew.yml.erb
84
+ - lib/gembrew/version.rb
85
+ homepage: https://github.com/DannyBen/gembrew
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ bug_tracker_uri: https://github.com/DannyBen/gembrew/issues
90
+ changelog_uri: https://github.com/DannyBen/gembrew/blob/master/CHANGELOG.md
91
+ source_code_uri: https://github.com/DannyBen/gembrew
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '3.3'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 4.0.17
108
+ specification_version: 4
109
+ summary: Homebrew formula generator for Ruby gems
110
+ test_files: []