[ruby/rubygems] Add --ext=go to bundle gem

(https://github.com/ruby/rubygems/pull/8183)

* Add new gem templates

* Add `--ext=go` in `bundle gem`

* Add setup-go to .github/workflows/main.yml

* Embed go version in go.mod

* Use go in bundler CI

* Add example method to template

* Install Go in .circleci/config.yml

* Install Go in .gitlab-ci.yml

* Allow hard tabs in go template

* Run `rake update_manifest`

* Fix test

* Move go_gem to gemspec

Respect to 9b0ec80

* nits: 

* includes valid module name in go.mod

* generate header file

* Run `go mod tidy` to create `go.sum`

* Check if `go.sum` is generated only when Go is installed

To avoid test failure in environments where Go is not installed

* Run CI

* Workaround for hung up

c.f. https://github.com/rubygems/rubygems/actions/runs/11639408044/job/32415545422

* Write man for --ext=go

* Re-generate man with `./bin/rake man:build`

* pinning 📌

* Update with `./bin/rake man:build`

* nits: Extract to method

* nits: Use `sys_exec` instead of `system`

* Clean go module cache after test

Workaround following error

```
1) bundle gem gem naming with underscore --ext parameter set with go includes go_gem extension in extconf.rb
   Failure/Error: FileUtils.rm_r(dir)

   Errno::EACCES:
     Permission denied @ apply2files - /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go
   # ./spec/support/helpers.rb:37:in `block in reset!'
   # ./spec/support/helpers.rb:21:in `each'
   # ./spec/support/helpers.rb:21:in `reset!'
   # ./spec/spec_helper.rb:130:in `block (2 levels) in <top (required)>'
   # /home/runner/work/rubygems/rubygems/lib/rubygems.rb:303:in `load'
   # /home/runner/work/rubygems/rubygems/lib/rubygems.rb:303:in `activate_and_load_bin_path'
```

Files installed with `go get` have permissions set to 444
ref. https://github.com/golang/go/issues/35615

```
$ ls -l /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go
-r--r--r-- 1 runner runner 42320 Nov 15 06:38 /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go
```

So they cannot be deleted by `FileUtils.rm_r`.
Therefore, this is necessary to execute `go clean -modcache` separately from `FileUtils.rm_r` to circumvent it.

* Remove needless changes

ref. https://github.com/ruby/rubygems/pull/8183#discussion_r2532902051

* ci: setup-go is needless

* Don't run go command in `bundle gem`

ref. https://github.com/ruby/rubygems/pull/8183#discussion_r2532765470

* Revert unrelated date changes

---------

https://github.com/ruby/rubygems/commit/260d7d60b3

Co-authored-by: Hiroshi SHIBATA <hsbt@ruby-lang.org>
This commit is contained in:
Go Sueyoshi 2025-11-19 09:46:35 +09:00 committed by git
parent 0f89fa97e3
commit 4423facbff
14 changed files with 254 additions and 7 deletions

View File

@ -11,7 +11,7 @@ module Bundler
AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze
PARSEABLE_COMMANDS = %w[check config help exec platform show version].freeze
EXTENSIONS = ["c", "rust"].freeze
EXTENSIONS = ["c", "rust", "go"].freeze
COMMAND_ALIASES = {
"check" => "c",

View File

@ -13,6 +13,8 @@ module Bundler
"test-unit" => "3.0",
}.freeze
DEFAULT_GITHUB_USERNAME = "[USERNAME]"
attr_reader :options, :gem_name, :thor, :name, :target, :extension
def initialize(options, gem_name, thor)
@ -72,7 +74,7 @@ module Bundler
bundle: options[:bundle],
bundler_version: bundler_dependency_version,
git: use_git,
github_username: github_username.empty? ? "[USERNAME]" : github_username,
github_username: github_username.empty? ? DEFAULT_GITHUB_USERNAME : github_username,
required_ruby_version: required_ruby_version,
rust_builder_required_rubygems_version: rust_builder_required_rubygems_version,
minitest_constant_name: minitest_constant_name,
@ -231,6 +233,18 @@ module Bundler
)
end
if extension == "go"
templates.merge!(
"ext/newgem/go.mod.tt" => "ext/#{name}/go.mod",
"ext/newgem/extconf-go.rb.tt" => "ext/#{name}/extconf.rb",
"ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
"ext/newgem/newgem.go.tt" => "ext/#{name}/#{underscored_name}.go",
"ext/newgem/newgem-go.c.tt" => "ext/#{name}/#{underscored_name}.c",
)
config[:go_module_username] = config[:github_username] == DEFAULT_GITHUB_USERNAME ? "username" : config[:github_username]
end
if target.exist? && !target.directory?
Bundler.ui.error "Couldn't create a new gem named `#{gem_name}` because there's an existing file named `#{gem_name}`."
exit Bundler::BundlerError.all_errors[Bundler::GenericSystemCallError]

View File

@ -38,8 +38,8 @@ Add a \fBCHANGELOG\.md\fR file to the root of the generated project\. If this op
\fB\-\-no\-changelog\fR
Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\.
.TP
\fB\-\-ext=c\fR, \fB\-\-ext=rust\fR
Add boilerplate for C or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\.
\fB\-\-ext=c\fR, \fB\-\-ext=go\fR, \fB\-\-ext=rust\fR
Add boilerplate for C, Go (currently go\-gem\-wrapper \fIhttps://github\.com/ruby\-go\-gem/go\-gem\-wrapper\fR based) or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\.
.TP
\fB\-\-no\-ext\fR
Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\.

View File

@ -51,8 +51,8 @@ configuration file using the following names:
Do not create a `CHANGELOG.md` (overrides `--changelog` specified in the
global config).
* `--ext=c`, `--ext=rust`:
Add boilerplate for C or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior
* `--ext=c`, `--ext=go`, `--ext=rust`:
Add boilerplate for C, Go (currently [go-gem-wrapper](https://github.com/ruby-go-gem/go-gem-wrapper) based) or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior
is disabled by default.
* `--no-ext`:

View File

@ -6,6 +6,10 @@ jobs:
<%- if config[:ext] == 'rust' -%>
environment:
RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
<%- end -%>
<%- if config[:ext] == 'go' -%>
environment:
GO_VERSION: '1.23.0'
<%- end -%>
steps:
- checkout
@ -16,6 +20,14 @@ jobs:
- run:
name: Install a RubyGems version that can compile rust extensions
command: gem update --system '<%= ::Gem.rubygems_version %>'
<%- end -%>
<%- if config[:ext] == 'go' -%>
- run:
name: Install Go
command: |
wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
tar -C /usr/local -xzf /tmp/go.tar.gz
echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV"
<%- end -%>
- run:
name: Run the default task

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require "mkmf"
require "go_gem/mkmf"
# Makes all symbols private by default to avoid unintended conflict
# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
# selectively, or entirely remove this flag.
append_cflags("-fvisibility=hidden")
create_go_makefile(<%= config[:makefile_path].inspect %>)

View File

@ -0,0 +1,5 @@
module github.com/<%= config[:go_module_username] %>/<%= config[:underscored_name] %>
go 1.23
require github.com/ruby-go-gem/go-gem-wrapper latest

View File

@ -0,0 +1,2 @@
#include "<%= config[:underscored_name] %>.h"
#include "_cgo_export.h"

View File

@ -0,0 +1,31 @@
package main
/*
#include "<%= config[:underscored_name] %>.h"
VALUE rb_<%= config[:underscored_name] %>_sum(VALUE self, VALUE a, VALUE b);
*/
import "C"
import (
"github.com/ruby-go-gem/go-gem-wrapper/ruby"
)
//export rb_<%= config[:underscored_name] %>_sum
func rb_<%= config[:underscored_name] %>_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE {
longA := ruby.NUM2LONG(ruby.VALUE(a))
longB := ruby.NUM2LONG(ruby.VALUE(b))
sum := longA + longB
return C.VALUE(ruby.LONG2NUM(sum))
}
//export Init_<%= config[:underscored_name] %>
func Init_<%= config[:underscored_name] %>() {
rb_m<%= config[:constant_array].join %> := ruby.RbDefineModule(<%= config[:constant_name].inspect %>)
ruby.RbDefineSingletonMethod(rb_m<%= config[:constant_array].join %>, "sum", C.rb_<%= config[:underscored_name] %>_sum, 2)
}
func main() {
}

View File

@ -34,6 +34,12 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
<%- end -%>
<%- if config[:ext] == 'go' -%>
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: ext/<%= config[:underscored_name] %>/go.mod
<%- end -%>
- name: Run the default task
run: bundle exec rake

View File

@ -5,6 +5,11 @@ default:
<%- if config[:ext] == 'rust' -%>
- apt-get update && apt-get install -y clang
- gem update --system '<%= ::Gem.rubygems_version %>'
<%- end -%>
<%- if config[:ext] == 'go' -%>
- wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
- tar -C /usr/local -xzf /tmp/go.tar.gz
- export PATH=/usr/local/go/bin:$PATH
<%- end -%>
- gem install bundler -v <%= Bundler::VERSION %>
- bundle install
@ -13,6 +18,10 @@ example_job:
<%- if config[:ext] == 'rust' -%>
variables:
RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
<%- end -%>
<%- if config[:ext] == 'go' -%>
variables:
GO_VERSION: '1.23.0'
<%- end -%>
script:
- bundle exec rake

View File

@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
<%- if config[:ext] == 'c' || config[:ext] == 'rust' -%>
<%- if %w(c rust go).include?(config[:ext]) -%>
spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"]
<%- end -%>
@ -47,6 +47,9 @@ Gem::Specification.new do |spec|
<%- if config[:ext] == 'rust' -%>
spec.add_dependency "rb_sys", "~> 0.9.91"
<%- end -%>
<%- if config[:ext] == 'go' -%>
spec.add_dependency "go_gem", "~> 0.2"
<%- end -%>
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html

View File

@ -31,6 +31,13 @@ RSpec.describe "bundle gem" do
matched[:ignored]&.split(" ")
end
def installed_go?
sys_exec("go version", raise_on_error: true)
true
rescue StandardError
false
end
let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) }
let(:gem_name) { "mygem" }
@ -1748,6 +1755,150 @@ RSpec.describe "bundle gem" do
expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
end
end
context "--ext parameter set with go" do
let(:flags) { "--ext=go" }
before do
bundle ["gem", gem_name, flags].compact.join(" ")
end
after do
sys_exec("go clean -modcache", raise_on_error: true) if installed_go?
end
it "is not deprecated" do
expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated."
end
it "builds ext skeleton" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist
end
it "includes extconf.rb in gem_name.gemspec" do
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"]))
end
it "includes go_gem in gem_name.gemspec" do
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", "~> 0.2"')
end
it "includes go_gem extension in extconf.rb" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY)
require "mkmf"
require "go_gem/mkmf"
RUBY
expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}")))
expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile")
end
it "includes go_gem extension in gem_name.c" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C)
#include "#{gem_name}.h"
#include "_cgo_export.h"
C
end
it "includes skeleton code in gem_name.go" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
/*
#include "#{gem_name}.h"
VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b);
*/
import "C"
GO
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
//export rb_#{gem_name}_sum
func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE {
GO
expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
//export Init_#{gem_name}
func Init_#{gem_name}() {
GO
end
it "includes valid module name in go.mod" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}")
end
context "with --no-ci" do
let(:flags) { "--ext=go --no-ci" }
it_behaves_like "CI config is absent"
end
context "--ci set to github" do
let(:flags) { "--ext=go --ci=github" }
it "generates .github/workflows/main.yml" do
expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod")
end
end
context "--ci set to circle" do
let(:flags) { "--ext=go --ci=circle" }
it "generates a .circleci/config.yml" do
expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip)
environment:
GO_VERSION:
YAML
expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML)
- run:
name: Install Go
command: |
wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
tar -C /usr/local -xzf /tmp/go.tar.gz
echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV"
YAML
end
end
context "--ci set to gitlab" do
let(:flags) { "--ext=go --ci=gitlab" }
it "generates a .gitlab-ci.yml" do
expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML)
- wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
- tar -C /usr/local -xzf /tmp/go.tar.gz
- export PATH=/usr/local/go/bin:$PATH
YAML
expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip)
variables:
GO_VERSION:
YAML
end
end
context "without github.user" do
before do
# FIXME: GitHub Actions Windows Runner hang up here for some reason...
skip "Workaround for hung up" if Gem.win_platform?
git("config --global --unset github.user")
bundle ["gem", gem_name, flags].compact.join(" ")
end
it "includes valid module name in go.mod" do
expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}")
end
end
end
end
context "gem naming with dashed" do

View File

@ -20,6 +20,9 @@ RSpec.describe "The library itself" do
end
def check_for_tab_characters(filename)
# Because Go uses hard tabs
return if filename.end_with?(".go.tt")
failing_lines = []
each_line(filename) do |line, number|
failing_lines << number + 1 if line.include?("\t")