Skip to content

Commit 3fb47ca

Browse files
Ruby: Add ability to install gem from a branch (#1337)
While working on writing PRs to Herb, I've run into issues installing my fork branches via `gem "herb", github:...`. --------- Co-authored-by: Marco Roth <marco.roth@intergga.ch>
1 parent 966e3ca commit 3fb47ca

File tree

8 files changed

+197
-30
lines changed

8 files changed

+197
-30
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ docs/docs/linter/rules/
1212
/ext/herb/extconf.h
1313
/ext/herb/herb.bundle
1414
/ext/herb/Makefile
15+
/ext/herb/mkmf.log
1516
/lib/herb/herb.bundle
1617

1718
# Prerequisites

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ Install the Herb gem via RubyGems:
8181
gem install herb
8282
```
8383

84+
### Installing from a Git branch
85+
86+
To test a branch before it's released (e.g. from a fork), add both `prism` and `herb` to your Gemfile:
87+
88+
```ruby
89+
gem "prism", github: "ruby/prism", tag: "v1.9.0"
90+
gem "herb", github: "fork/herb", branch: "my-branch"
91+
```
92+
93+
The `prism` gem is required because Herb's native C extension compiles against
94+
Prism's C source, which is vendored automatically during installation.
95+
8496
For detailed information, like how you can use Herb programmatically in Ruby and JavaScript, visit the [documentation site](https://herb-tools.dev/bindings/ruby/reference).
8597

8698
Basic usage to analyze all HTML+ERB files in your project:

Rakefile

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -149,55 +149,35 @@ end
149149

150150
desc "Render out template files"
151151
task :templates do
152-
require_relative "templates/template"
152+
require_relative "lib/herb/bootstrap"
153153

154-
Dir.glob("#{__dir__}/templates/**/*.erb").each do |template|
155-
Herb::Template.render(template)
156-
end
154+
Herb::Bootstrap.generate_templates
157155
end
158156

159-
prism_vendor_path = "vendor/prism"
160-
161157
namespace :prism do
162158
desc "Setup and vendor Prism"
163159
task :vendor do
160+
require_relative "lib/herb/bootstrap"
161+
164162
Rake::Task["prism:clean"].execute
165163

166164
prism_bundle_path = `bundle show prism`.chomp
167165

168-
puts prism_bundle_path
169-
170166
if prism_bundle_path.empty?
171167
puts "Make sure to run `bundle install` in the herb project directory first"
172168
exit 1
173169
end
174170

175-
FileUtils.mkdir_p(prism_vendor_path)
176-
177-
files = [
178-
"config.yml",
179-
"Rakefile",
180-
"src/",
181-
"include/",
182-
"templates/"
183-
]
184-
185-
files.each do |file|
186-
vendored_file_path = prism_vendor_path + "/#{file}"
187-
puts "Vendoring '#{file}' Prism file to #{vendored_file_path}"
188-
FileUtils.cp_r(prism_bundle_path + "/#{file}", prism_vendor_path)
189-
end
190-
191-
prism_ast_header = "#{prism_vendor_path}/include/prism/ast.h"
171+
puts prism_bundle_path
192172

193-
unless File.exist?(prism_ast_header)
194-
puts "Generating Prism template files..."
195-
system("ruby #{prism_vendor_path}/templates/template.rb", exception: true)
196-
end
173+
Herb::Bootstrap.vendor_prism(prism_gem_path: prism_bundle_path)
197174
end
198175

199176
desc "Clean vendored Prism in vendor/prism/"
200177
task :clean do
178+
require_relative "lib/herb/bootstrap"
179+
180+
prism_vendor_path = Herb::Bootstrap::PRISM_VENDOR_DIR
201181
puts "Cleaning up vendored Prism at #{prism_vendor_path}..."
202182
begin
203183
FileUtils.rm_r(prism_vendor_path)

Steepfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ target :lib do
1616
ignore "lib/herb/cli.rb"
1717
ignore "lib/herb/project.rb"
1818
ignore "lib/herb/engine/error_formatter.rb"
19+
ignore "lib/herb/bootstrap.rb"
1920
end

ext/herb/extconf.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
11
# frozen_string_literal: true
22

33
require "mkmf"
4+
require_relative "../../lib/herb/bootstrap"
45

56
extension_name = "herb"
67

8+
if Herb::Bootstrap.git_source? && !Herb::Bootstrap.templates_generated?
9+
puts "Building from source — running bootstrap..."
10+
Herb::Bootstrap.generate_templates
11+
12+
unless Herb::Bootstrap.prism_vendored?
13+
prism_path = Herb::Bootstrap.find_prism_gem_path
14+
15+
abort <<~MSG unless prism_path
16+
ERROR: Could not find Prism C source files.
17+
18+
When installing Herb from a git source, a git-sourced Prism is required
19+
(the released gem does not include C source files).
20+
21+
Add it to your Gemfile before the herb git reference:
22+
23+
gem "prism", github: "ruby/prism", tag: "v1.9.0"
24+
gem "herb", github: "...", branch: "..."
25+
26+
Then run `bundle install` again.
27+
MSG
28+
29+
puts "Vendoring Prism from #{prism_path}..."
30+
Herb::Bootstrap.vendor_prism(prism_gem_path: prism_path)
31+
end
32+
33+
root_path = Herb::Bootstrap::ROOT_PATH
34+
sha = `git -C #{root_path} rev-parse --short HEAD 2>/dev/null`.strip
35+
36+
$CFLAGS << " -DHERB_GIT_BUILD"
37+
$CFLAGS << " -DHERB_GIT_SHA=\\\"#{sha}\\\"" unless sha.empty?
38+
end
39+
740
include_path = File.expand_path("../../src/include", __dir__)
841
prism_path = File.expand_path("../../vendor/prism", __dir__)
942

ext/herb/extension.c

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,31 @@ static VALUE Herb_version(VALUE self) {
359359
VALUE gem_version = rb_const_get(self, rb_intern("VERSION"));
360360
VALUE libherb_version = rb_utf8_str_new_cstr(herb_version());
361361
VALUE libprism_version = rb_utf8_str_new_cstr(herb_prism_version());
362-
VALUE format_string = rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)");
362+
363+
#ifdef HERB_GIT_BUILD
364+
# ifdef HERB_GIT_SHA
365+
VALUE format_string = rb_utf8_str_new_cstr(
366+
"herb gem " HERB_GIT_SHA ", libprism v%s, libherb " HERB_GIT_SHA " (Ruby C native extension, built from source)"
367+
);
368+
369+
return rb_funcall(rb_mKernel, rb_intern("sprintf"), 2, format_string, libprism_version);
370+
# else
371+
VALUE format_string =
372+
rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension, built from source)");
363373

364374
return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version);
375+
# endif
376+
#else
377+
return rb_funcall(
378+
rb_mKernel,
379+
rb_intern("sprintf"),
380+
4,
381+
rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)"),
382+
gem_version,
383+
libprism_version,
384+
libherb_version
385+
);
386+
#endif
365387
}
366388

367389
__attribute__((__visibility__("default"))) void Init_herb(void) {

lib/herb/bootstrap.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
# typed: ignore
3+
4+
require "fileutils"
5+
6+
module Herb
7+
module Bootstrap
8+
ROOT_PATH = File.expand_path("../..", __dir__)
9+
PRISM_VENDOR_DIR = File.join(ROOT_PATH, "vendor", "prism")
10+
11+
PRISM_ENTRIES = [
12+
"config.yml",
13+
"Rakefile",
14+
"src/",
15+
"include/",
16+
"templates/"
17+
].freeze
18+
19+
def self.generate_templates
20+
require "pathname"
21+
require "set"
22+
require_relative "../../templates/template"
23+
24+
Dir.chdir(ROOT_PATH) do
25+
Dir.glob("#{ROOT_PATH}/templates/**/*.erb").each do |template|
26+
Herb::Template.render(template)
27+
end
28+
end
29+
end
30+
31+
def self.git_source?
32+
File.directory?(File.join(ROOT_PATH, ".git"))
33+
end
34+
35+
def self.templates_generated?
36+
File.exist?(File.join(ROOT_PATH, "ext", "herb", "nodes.c"))
37+
end
38+
39+
def self.vendor_prism(prism_gem_path:)
40+
FileUtils.mkdir_p(PRISM_VENDOR_DIR)
41+
42+
PRISM_ENTRIES.each do |entry|
43+
source = File.join(prism_gem_path, entry)
44+
next unless File.exist?(source)
45+
46+
puts "Vendoring '#{entry}' Prism file to #{PRISM_VENDOR_DIR}/#{entry}"
47+
FileUtils.cp_r(source, PRISM_VENDOR_DIR)
48+
end
49+
50+
generate_prism_templates unless prism_ast_header_exists?
51+
end
52+
53+
def self.prism_vendored?
54+
File.directory?(File.join(PRISM_VENDOR_DIR, "include"))
55+
end
56+
57+
def self.prism_ast_header_exists?
58+
File.exist?(File.join(PRISM_VENDOR_DIR, "include", "prism", "ast.h"))
59+
end
60+
61+
def self.find_prism_gem_path
62+
find_prism_as_bundler_sibling || find_prism_from_gem_spec
63+
end
64+
65+
def self.generate_prism_templates
66+
puts "Generating Prism template files..."
67+
system("ruby", "#{PRISM_VENDOR_DIR}/templates/template.rb", exception: true)
68+
end
69+
70+
def self.find_prism_as_bundler_sibling
71+
bundler_gems_dir = File.expand_path("..", ROOT_PATH)
72+
candidates = Dir.glob(File.join(bundler_gems_dir, "prism-*"))
73+
74+
candidates.find { |path| File.directory?(File.join(path, "src")) }
75+
end
76+
77+
def self.find_prism_from_gem_spec
78+
path = Gem::Specification.find_by_name("prism").full_gem_path
79+
80+
return path if File.directory?(File.join(path, "src"))
81+
82+
nil
83+
rescue Gem::MissingSpecError
84+
nil
85+
end
86+
end
87+
end

sig/herb/bootstrap.rbs

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)