diff options
| author | Go Sueyoshi <sue445@users.noreply.github.com> | 2025-11-19 09:46:35 +0900 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2025-11-19 00:47:07 +0000 |
| commit | 4423facbffd5bd523541ebf42dc274272b1de732 (patch) | |
| tree | 933b284c5d54b31fd14069d11bf66c7ad8fc173d | |
| parent | 0f89fa97e3629af427282b5b6a800d2b97dd7d65 (diff) | |
[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: :golf:
* 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 :pushpin:
* 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>
| -rw-r--r-- | lib/bundler/cli.rb | 2 | ||||
| -rw-r--r-- | lib/bundler/cli/gem.rb | 16 | ||||
| -rw-r--r-- | lib/bundler/man/bundle-gem.1 | 4 | ||||
| -rw-r--r-- | lib/bundler/man/bundle-gem.1.ronn | 4 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/circleci/config.yml.tt | 12 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt | 11 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/ext/newgem/go.mod.tt | 5 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt | 2 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/ext/newgem/newgem.go.tt | 31 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/github/workflows/main.yml.tt | 6 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/gitlab-ci.yml.tt | 9 | ||||
| -rw-r--r-- | lib/bundler/templates/newgem/newgem.gemspec.tt | 5 | ||||
| -rw-r--r-- | spec/bundler/commands/newgem_spec.rb | 151 | ||||
| -rw-r--r-- | spec/bundler/quality_spec.rb | 3 |
14 files changed, 254 insertions, 7 deletions
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index af634291dd..86b86b359f 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -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", diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index abfb095d46..236ce530ec 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -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] diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index 670a69d67e..85c0f57674 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -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)\. diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn index b71bde9f65..488c8113e4 100644 --- a/lib/bundler/man/bundle-gem.1.ronn +++ b/lib/bundler/man/bundle-gem.1.ronn @@ -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`: diff --git a/lib/bundler/templates/newgem/circleci/config.yml.tt b/lib/bundler/templates/newgem/circleci/config.yml.tt index f40f029bf1..c4dd9d0647 100644 --- a/lib/bundler/templates/newgem/circleci/config.yml.tt +++ b/lib/bundler/templates/newgem/circleci/config.yml.tt @@ -7,6 +7,10 @@ jobs: environment: RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' <%- end -%> +<%- if config[:ext] == 'go' -%> + environment: + GO_VERSION: '1.23.0' +<%- end -%> steps: - checkout <%- if config[:ext] == 'rust' -%> @@ -17,6 +21,14 @@ jobs: 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 command: | diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt new file mode 100644 index 0000000000..a689e21ebe --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt @@ -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 %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/go.mod.tt b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt new file mode 100644 index 0000000000..3f4819d004 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt @@ -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 diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt new file mode 100644 index 0000000000..119c0c96ea --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt @@ -0,0 +1,2 @@ +#include "<%= config[:underscored_name] %>.h" +#include "_cgo_export.h" diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt new file mode 100644 index 0000000000..f19b750e58 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt @@ -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() { +} diff --git a/lib/bundler/templates/newgem/github/workflows/main.yml.tt b/lib/bundler/templates/newgem/github/workflows/main.yml.tt index 9224ee0ca2..7f3e3a5b66 100644 --- a/lib/bundler/templates/newgem/github/workflows/main.yml.tt +++ b/lib/bundler/templates/newgem/github/workflows/main.yml.tt @@ -35,5 +35,11 @@ jobs: 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 diff --git a/lib/bundler/templates/newgem/gitlab-ci.yml.tt b/lib/bundler/templates/newgem/gitlab-ci.yml.tt index d2e1f33736..adbd70cbc0 100644 --- a/lib/bundler/templates/newgem/gitlab-ci.yml.tt +++ b/lib/bundler/templates/newgem/gitlab-ci.yml.tt @@ -6,6 +6,11 @@ default: - 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 @@ -14,5 +19,9 @@ example_job: variables: RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' <%- end -%> +<%- if config[:ext] == 'go' -%> + variables: + GO_VERSION: '1.23.0' +<%- end -%> script: - bundle exec rake diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt index c87abda8a0..513875fd63 100644 --- a/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -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 diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index 1ce4a0da09..7a837bd08f 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -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 diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb index bbd6517f21..b60be9980f 100644 --- a/spec/bundler/quality_spec.rb +++ b/spec/bundler/quality_spec.rb @@ -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") |
