diff --git a/.github/workflows/bundler.yml b/.github/workflows/bundler.yml index effdcd2c3337..224666306ba0 100644 --- a/.github/workflows/bundler.yml +++ b/.github/workflows/bundler.yml @@ -68,6 +68,10 @@ jobs: distribution: temurin java-version: 19.0.2 if: matrix.os.name == 'Windows' && matrix.ruby.name == 'jruby' + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: stable - name: Install graphviz (Ubuntu) run: sudo apt-get install graphviz -y if: matrix.bundler.value == '' && matrix.os.name == 'Ubuntu' diff --git a/Manifest.txt b/Manifest.txt index a4391b9fedc8..fc73d922616d 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -226,8 +226,12 @@ bundler/lib/bundler/templates/newgem/circleci/config.yml.tt bundler/lib/bundler/templates/newgem/exe/newgem.tt bundler/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt bundler/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt +bundler/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt bundler/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt +bundler/lib/bundler/templates/newgem/ext/newgem/go.mod.tt +bundler/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt bundler/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt +bundler/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt bundler/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt bundler/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt bundler/lib/bundler/templates/newgem/github/workflows/main.yml.tt diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index 91c31651da31..df3be8b922da 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -11,7 +11,7 @@ class CLI < Thor 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/bundler/lib/bundler/cli/gem.rb b/bundler/lib/bundler/cli/gem.rb index 22bcf0e47aa2..aa9f7dd32857 100644 --- a/bundler/lib/bundler/cli/gem.rb +++ b/bundler/lib/bundler/cli/gem.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "pathname" +require "open3" module Bundler class CLI @@ -15,6 +16,8 @@ class CLI::Gem "test-unit" => "3.0", }.freeze + DEFAULT_GITHUB_USERNAME = "[USERNAME]" + attr_reader :options, :gem_name, :thor, :name, :target, :extension def initialize(options, gem_name, thor) @@ -71,7 +74,7 @@ def run exe: options[:exe], 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, @@ -213,6 +216,19 @@ def run ) 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_version] = go_version + 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] @@ -237,6 +253,10 @@ def run path.chmod(executable) end + if extension == "go" + run_go_mod_tidy(target.join("ext/#{name}")) + end + if use_git IO.popen(%w[git add .], { chdir: target }, &:read) end @@ -463,5 +483,31 @@ def validate_rust_builder_rubygems_version exit 1 end end + + def go_version + stdout, _, status = Open3.capture3("go version") + + # Suppress error if Go isn't installed + return nil unless status.success? + + /go version go([.\d]+)/.match(stdout)[1] + end + + # Run `go mod tidy` within ext/newgem/ + def run_go_mod_tidy(ext_dir) + Dir.chdir(ext_dir) do + _, stderr, status = Open3.capture3("go mod tidy") + + if status.success? + Bundler.ui.info "#{ext_dir}/go.sum has been created with `go mod tidy`" + else + Bundler.ui.warn <<~MSG + An error occurred when executing `go mod tidy`. + stderr: #{stderr} + Please run `go mod tidy` later in #{ext_dir} to create `go.sum`. + MSG + end + end + end end end diff --git a/bundler/lib/bundler/man/bundle-gem.1 b/bundler/lib/bundler/man/bundle-gem.1 index 65882afa4fa6..c5b57d36b25f 100644 --- a/bundler/lib/bundler/man/bundle-gem.1 +++ b/bundler/lib/bundler/man/bundle-gem.1 @@ -32,7 +32,7 @@ The generated project skeleton can be customized with OPTIONS, as explained belo .IP "\(bu" 4 \fB\-\-no\-changelog\fR: Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\. .IP "\(bu" 4 -\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\. .IP "\(bu" 4 \fB\-\-no\-ext\fR: Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\. .IP "\(bu" 4 diff --git a/bundler/lib/bundler/man/bundle-gem.1.ronn b/bundler/lib/bundler/man/bundle-gem.1.ronn index 13dc55c3103f..eb68636f66a2 100644 --- a/bundler/lib/bundler/man/bundle-gem.1.ronn +++ b/bundler/lib/bundler/man/bundle-gem.1.ronn @@ -50,8 +50,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/bundler/lib/bundler/templates/newgem/circleci/config.yml.tt b/bundler/lib/bundler/templates/newgem/circleci/config.yml.tt index f40f029bf130..c4dd9d06471e 100644 --- a/bundler/lib/bundler/templates/newgem/circleci/config.yml.tt +++ b/bundler/lib/bundler/templates/newgem/circleci/config.yml.tt @@ -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 diff --git a/bundler/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt b/bundler/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt new file mode 100644 index 000000000000..a689e21ebe9c --- /dev/null +++ b/bundler/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/bundler/lib/bundler/templates/newgem/ext/newgem/go.mod.tt b/bundler/lib/bundler/templates/newgem/ext/newgem/go.mod.tt new file mode 100644 index 000000000000..96b94078cf59 --- /dev/null +++ b/bundler/lib/bundler/templates/newgem/ext/newgem/go.mod.tt @@ -0,0 +1,5 @@ +module github.com/<%= config[:go_module_username] %>/<%= config[:underscored_name] %> + +go <%= config[:go_version] %> + +require github.com/ruby-go-gem/go-gem-wrapper latest diff --git a/bundler/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt b/bundler/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt new file mode 100644 index 000000000000..119c0c96ea5a --- /dev/null +++ b/bundler/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/bundler/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt b/bundler/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt new file mode 100644 index 000000000000..f19b750e58c4 --- /dev/null +++ b/bundler/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/bundler/lib/bundler/templates/newgem/github/workflows/main.yml.tt b/bundler/lib/bundler/templates/newgem/github/workflows/main.yml.tt index d1b5ae05343e..de79afc7a03d 100644 --- a/bundler/lib/bundler/templates/newgem/github/workflows/main.yml.tt +++ b/bundler/lib/bundler/templates/newgem/github/workflows/main.yml.tt @@ -32,6 +32,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 diff --git a/bundler/lib/bundler/templates/newgem/gitlab-ci.yml.tt b/bundler/lib/bundler/templates/newgem/gitlab-ci.yml.tt index d2e1f337362f..adbd70cbc050 100644 --- a/bundler/lib/bundler/templates/newgem/gitlab-ci.yml.tt +++ b/bundler/lib/bundler/templates/newgem/gitlab-ci.yml.tt @@ -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 diff --git a/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt b/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt index ced300f37911..2d3975b0841c 100644 --- a/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -37,7 +37,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 -%> @@ -46,6 +46,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/bundler/spec/commands/newgem_spec.rb b/bundler/spec/commands/newgem_spec.rb index a7fe31143def..cadc3386f293 100644 --- a/bundler/spec/commands/newgem_spec.rb +++ b/bundler/spec/commands/newgem_spec.rb @@ -1622,6 +1622,164 @@ def create_temporary_dir(dir) 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 + + 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 "when Go is installed" do + before do + skip "Go isn't installed" unless system("go version") + end + + let(:go_version) do + /go version go([.\d]+)/.match(`go version`)[1] + end + + it "includes go version in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("go #{go_version}") + end + + it "go.sum is generated" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.sum")).to exist + 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/bundler/spec/quality_spec.rb b/bundler/spec/quality_spec.rb index c7fce17b6277..8a3ba7a0fd5c 100644 --- a/bundler/spec/quality_spec.rb +++ b/bundler/spec/quality_spec.rb @@ -20,6 +20,9 @@ def check_for_git_merge_conflicts(filename) 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")