Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ docs/docs/linter/rules/
/ext/herb/extconf.h
/ext/herb/herb.bundle
/ext/herb/Makefile
/ext/herb/mkmf.log
/lib/herb/herb.bundle

# Prerequisites
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ Install the Herb gem via RubyGems:
gem install herb
```

### Installing from a Git branch

To test a branch before it's released (e.g. from a fork), add both `prism` and `herb` to your Gemfile:

```ruby
gem "prism", github: "ruby/prism", tag: "v1.9.0"
gem "herb", github: "fork/herb", branch: "my-branch"
```

The `prism` gem is required because Herb's native C extension compiles against
Prism's C source, which is vendored automatically during installation.

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).

Basic usage to analyze all HTML+ERB files in your project:
Expand Down
38 changes: 9 additions & 29 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -149,55 +149,35 @@ end

desc "Render out template files"
task :templates do
require_relative "templates/template"
require_relative "lib/herb/bootstrap"

Dir.glob("#{__dir__}/templates/**/*.erb").each do |template|
Herb::Template.render(template)
end
Herb::Bootstrap.generate_templates
end

prism_vendor_path = "vendor/prism"

namespace :prism do
desc "Setup and vendor Prism"
task :vendor do
require_relative "lib/herb/bootstrap"

Rake::Task["prism:clean"].execute

prism_bundle_path = `bundle show prism`.chomp

puts prism_bundle_path

if prism_bundle_path.empty?
puts "Make sure to run `bundle install` in the herb project directory first"
exit 1
end

FileUtils.mkdir_p(prism_vendor_path)

files = [
"config.yml",
"Rakefile",
"src/",
"include/",
"templates/"
]

files.each do |file|
vendored_file_path = prism_vendor_path + "/#{file}"
puts "Vendoring '#{file}' Prism file to #{vendored_file_path}"
FileUtils.cp_r(prism_bundle_path + "/#{file}", prism_vendor_path)
end

prism_ast_header = "#{prism_vendor_path}/include/prism/ast.h"
puts prism_bundle_path

unless File.exist?(prism_ast_header)
puts "Generating Prism template files..."
system("ruby #{prism_vendor_path}/templates/template.rb", exception: true)
end
Herb::Bootstrap.vendor_prism(prism_gem_path: prism_bundle_path)
end

desc "Clean vendored Prism in vendor/prism/"
task :clean do
require_relative "lib/herb/bootstrap"

prism_vendor_path = Herb::Bootstrap::PRISM_VENDOR_DIR
puts "Cleaning up vendored Prism at #{prism_vendor_path}..."
begin
FileUtils.rm_r(prism_vendor_path)
Expand Down
1 change: 1 addition & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ target :lib do
ignore "lib/herb/cli.rb"
ignore "lib/herb/project.rb"
ignore "lib/herb/engine/error_formatter.rb"
ignore "lib/herb/bootstrap.rb"
end
33 changes: 33 additions & 0 deletions ext/herb/extconf.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
# frozen_string_literal: true

require "mkmf"
require_relative "../../lib/herb/bootstrap"

extension_name = "herb"

if Herb::Bootstrap.git_source? && !Herb::Bootstrap.templates_generated?
puts "Building from source — running bootstrap..."
Herb::Bootstrap.generate_templates

unless Herb::Bootstrap.prism_vendored?
prism_path = Herb::Bootstrap.find_prism_gem_path

abort <<~MSG unless prism_path
ERROR: Could not find Prism C source files.

When installing Herb from a git source, a git-sourced Prism is required
(the released gem does not include C source files).

Add it to your Gemfile before the herb git reference:

gem "prism", github: "ruby/prism", tag: "v1.9.0"
gem "herb", github: "...", branch: "..."

Then run `bundle install` again.
MSG

puts "Vendoring Prism from #{prism_path}..."
Herb::Bootstrap.vendor_prism(prism_gem_path: prism_path)
end

root_path = Herb::Bootstrap::ROOT_PATH
sha = `git -C #{root_path} rev-parse --short HEAD 2>/dev/null`.strip

$CFLAGS << " -DHERB_GIT_BUILD"
$CFLAGS << " -DHERB_GIT_SHA=\\\"#{sha}\\\"" unless sha.empty?
end

include_path = File.expand_path("../../src/include", __dir__)
prism_path = File.expand_path("../../vendor/prism", __dir__)

Expand Down
24 changes: 23 additions & 1 deletion ext/herb/extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,31 @@ static VALUE Herb_version(VALUE self) {
VALUE gem_version = rb_const_get(self, rb_intern("VERSION"));
VALUE libherb_version = rb_utf8_str_new_cstr(herb_version());
VALUE libprism_version = rb_utf8_str_new_cstr(herb_prism_version());
VALUE format_string = rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)");

#ifdef HERB_GIT_BUILD
# ifdef HERB_GIT_SHA
VALUE format_string = rb_utf8_str_new_cstr(
"herb gem " HERB_GIT_SHA ", libprism v%s, libherb " HERB_GIT_SHA " (Ruby C native extension, built from source)"
);

return rb_funcall(rb_mKernel, rb_intern("sprintf"), 2, format_string, libprism_version);
# else
VALUE format_string =
rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension, built from source)");

return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version);
# endif
#else
return rb_funcall(
rb_mKernel,
rb_intern("sprintf"),
4,
rb_utf8_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)"),
gem_version,
libprism_version,
libherb_version
);
#endif
}

__attribute__((__visibility__("default"))) void Init_herb(void) {
Expand Down
87 changes: 87 additions & 0 deletions lib/herb/bootstrap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true
# typed: ignore

require "fileutils"

module Herb
module Bootstrap
ROOT_PATH = File.expand_path("../..", __dir__)
PRISM_VENDOR_DIR = File.join(ROOT_PATH, "vendor", "prism")

PRISM_ENTRIES = [
"config.yml",
"Rakefile",
"src/",
"include/",
"templates/"
].freeze

def self.generate_templates
require "pathname"
require "set"
require_relative "../../templates/template"

Dir.chdir(ROOT_PATH) do
Dir.glob("#{ROOT_PATH}/templates/**/*.erb").each do |template|
Herb::Template.render(template)
end
end
end

def self.git_source?
File.directory?(File.join(ROOT_PATH, ".git"))
end

def self.templates_generated?
File.exist?(File.join(ROOT_PATH, "ext", "herb", "nodes.c"))
end

def self.vendor_prism(prism_gem_path:)
FileUtils.mkdir_p(PRISM_VENDOR_DIR)

PRISM_ENTRIES.each do |entry|
source = File.join(prism_gem_path, entry)
next unless File.exist?(source)

puts "Vendoring '#{entry}' Prism file to #{PRISM_VENDOR_DIR}/#{entry}"
FileUtils.cp_r(source, PRISM_VENDOR_DIR)
end

generate_prism_templates unless prism_ast_header_exists?
end

def self.prism_vendored?
File.directory?(File.join(PRISM_VENDOR_DIR, "include"))
end

def self.prism_ast_header_exists?
File.exist?(File.join(PRISM_VENDOR_DIR, "include", "prism", "ast.h"))
end

def self.find_prism_gem_path
find_prism_as_bundler_sibling || find_prism_from_gem_spec
end

def self.generate_prism_templates
puts "Generating Prism template files..."
system("ruby", "#{PRISM_VENDOR_DIR}/templates/template.rb", exception: true)
end

def self.find_prism_as_bundler_sibling
bundler_gems_dir = File.expand_path("..", ROOT_PATH)
candidates = Dir.glob(File.join(bundler_gems_dir, "prism-*"))

candidates.find { |path| File.directory?(File.join(path, "src")) }
end

def self.find_prism_from_gem_spec
path = Gem::Specification.find_by_name("prism").full_gem_path

return path if File.directory?(File.join(path, "src"))

nil
rescue Gem::MissingSpecError
nil
end
end
end
31 changes: 31 additions & 0 deletions sig/herb/bootstrap.rbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading