mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
325 lines
11 KiB
Ruby
325 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "pathname"
|
|
|
|
module Bundler
|
|
class CLI
|
|
Bundler.require_thor_actions
|
|
include Thor::Actions
|
|
end
|
|
|
|
class CLI::Gem
|
|
TEST_FRAMEWORK_VERSIONS = {
|
|
"rspec" => "3.0",
|
|
"minitest" => "5.0",
|
|
"test-unit" => "3.0",
|
|
}.freeze
|
|
|
|
attr_reader :options, :gem_name, :thor, :name, :target
|
|
|
|
def initialize(options, gem_name, thor)
|
|
@options = options
|
|
@gem_name = resolve_name(gem_name)
|
|
|
|
@thor = thor
|
|
thor.behavior = :invoke
|
|
thor.destination_root = nil
|
|
|
|
@name = @gem_name
|
|
@target = SharedHelpers.pwd.join(gem_name)
|
|
|
|
validate_ext_name if options[:ext]
|
|
end
|
|
|
|
def run
|
|
Bundler.ui.confirm "Creating gem '#{name}'..."
|
|
|
|
underscored_name = name.tr("-", "_")
|
|
namespaced_path = name.tr("-", "/")
|
|
constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase }
|
|
constant_array = constant_name.split("::")
|
|
|
|
git_installed = Bundler.git_present?
|
|
|
|
git_author_name = git_installed ? `git config user.name`.chomp : ""
|
|
github_username = git_installed ? `git config github.user`.chomp : ""
|
|
git_user_email = git_installed ? `git config user.email`.chomp : ""
|
|
|
|
config = {
|
|
:name => name,
|
|
:underscored_name => underscored_name,
|
|
:namespaced_path => namespaced_path,
|
|
:makefile_path => "#{underscored_name}/#{underscored_name}",
|
|
:constant_name => constant_name,
|
|
:constant_array => constant_array,
|
|
:author => git_author_name.empty? ? "TODO: Write your name" : git_author_name,
|
|
:email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email,
|
|
:test => options[:test],
|
|
:ext => options[:ext],
|
|
:exe => options[:exe],
|
|
:bundler_version => bundler_dependency_version,
|
|
:github_username => github_username.empty? ? "[USERNAME]" : github_username,
|
|
}
|
|
ensure_safe_gem_name(name, constant_array)
|
|
|
|
templates = {
|
|
"#{Bundler.preferred_gemfile_name}.tt" => Bundler.preferred_gemfile_name,
|
|
"lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb",
|
|
"lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb",
|
|
"newgem.gemspec.tt" => "#{name}.gemspec",
|
|
"Rakefile.tt" => "Rakefile",
|
|
"README.md.tt" => "README.md",
|
|
"bin/console.tt" => "bin/console",
|
|
"bin/setup.tt" => "bin/setup",
|
|
}
|
|
|
|
executables = %w[
|
|
bin/console
|
|
bin/setup
|
|
]
|
|
|
|
templates.merge!("gitignore.tt" => ".gitignore") if Bundler.git_present?
|
|
|
|
if test_framework = ask_and_set_test_framework
|
|
config[:test] = test_framework
|
|
config[:test_framework_version] = TEST_FRAMEWORK_VERSIONS[test_framework]
|
|
|
|
case test_framework
|
|
when "rspec"
|
|
templates.merge!(
|
|
"rspec.tt" => ".rspec",
|
|
"spec/spec_helper.rb.tt" => "spec/spec_helper.rb",
|
|
"spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb"
|
|
)
|
|
config[:test_task] = :spec
|
|
when "minitest"
|
|
templates.merge!(
|
|
"test/minitest/test_helper.rb.tt" => "test/test_helper.rb",
|
|
"test/minitest/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb"
|
|
)
|
|
config[:test_task] = :test
|
|
when "test-unit"
|
|
templates.merge!(
|
|
"test/test-unit/test_helper.rb.tt" => "test/test_helper.rb",
|
|
"test/test-unit/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb"
|
|
)
|
|
config[:test_task] = :test
|
|
end
|
|
end
|
|
|
|
config[:ci] = ask_and_set_ci
|
|
case config[:ci]
|
|
when "github"
|
|
templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml")
|
|
when "travis"
|
|
templates.merge!("travis.yml.tt" => ".travis.yml")
|
|
when "gitlab"
|
|
templates.merge!("gitlab-ci.yml.tt" => ".gitlab-ci.yml")
|
|
when "circle"
|
|
templates.merge!("circleci/config.yml.tt" => ".circleci/config.yml")
|
|
end
|
|
|
|
if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?",
|
|
"This means that any other developer or company will be legally allowed to use your code " \
|
|
"for free as long as they admit you created it. You can read more about the MIT license " \
|
|
"at https://choosealicense.com/licenses/mit.")
|
|
config[:mit] = true
|
|
Bundler.ui.info "MIT License enabled in config"
|
|
templates.merge!("LICENSE.txt.tt" => "LICENSE.txt")
|
|
end
|
|
|
|
if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?",
|
|
"Codes of conduct can increase contributions to your project by contributors who " \
|
|
"prefer collaborative, safe spaces. You can read more about the code of conduct at " \
|
|
"contributor-covenant.org. Having a code of conduct means agreeing to the responsibility " \
|
|
"of enforcing it, so be sure that you are prepared to do that. Be sure that your email " \
|
|
"address is specified as a contact in the generated code of conduct so that people know " \
|
|
"who to contact in case of a violation. For suggestions about " \
|
|
"how to enforce codes of conduct, see https://bit.ly/coc-enforcement.")
|
|
config[:coc] = true
|
|
Bundler.ui.info "Code of conduct enabled in config"
|
|
templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md")
|
|
end
|
|
|
|
if ask_and_set(:rubocop, "Do you want to add rubocop as a dependency for gems you generate?",
|
|
"RuboCop is a static code analyzer that has out-of-the-box rules for many " \
|
|
"of the guidelines in the community style guide. " \
|
|
"For more information, see the RuboCop docs (https://docs.rubocop.org/en/stable/) " \
|
|
"and the Ruby Style Guides (https://github.com/rubocop-hq/ruby-style-guide).")
|
|
config[:rubocop] = true
|
|
Bundler.ui.info "RuboCop enabled in config"
|
|
templates.merge!("rubocop.yml.tt" => ".rubocop.yml")
|
|
end
|
|
|
|
templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe]
|
|
|
|
if options[:ext]
|
|
templates.merge!(
|
|
"ext/newgem/extconf.rb.tt" => "ext/#{name}/extconf.rb",
|
|
"ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
|
|
"ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c"
|
|
)
|
|
end
|
|
|
|
templates.each do |src, dst|
|
|
destination = target.join(dst)
|
|
SharedHelpers.filesystem_access(destination) do
|
|
thor.template("newgem/#{src}", destination, config)
|
|
end
|
|
end
|
|
|
|
executables.each do |file|
|
|
SharedHelpers.filesystem_access(target.join(file)) do |path|
|
|
executable = (path.stat.mode | 0o111)
|
|
path.chmod(executable)
|
|
end
|
|
end
|
|
|
|
if Bundler.git_present? && options[:git]
|
|
Bundler.ui.info "Initializing git repo in #{target}"
|
|
Dir.chdir(target) do
|
|
`git init`
|
|
`git add .`
|
|
end
|
|
end
|
|
|
|
# Open gemspec in editor
|
|
open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit]
|
|
|
|
Bundler.ui.info "Gem '#{name}' was successfully created. " \
|
|
"For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html"
|
|
rescue Errno::EEXIST => e
|
|
raise GenericSystemCallError.new(e, "There was a conflict while creating the new gem.")
|
|
end
|
|
|
|
private
|
|
|
|
def resolve_name(name)
|
|
SharedHelpers.pwd.join(name).basename.to_s
|
|
end
|
|
|
|
def ask_and_set(key, header, message)
|
|
choice = options[key]
|
|
choice = Bundler.settings["gem.#{key}"] if choice.nil?
|
|
|
|
if choice.nil?
|
|
Bundler.ui.confirm header
|
|
choice = Bundler.ui.yes? "#{message} y/(n):"
|
|
Bundler.settings.set_global("gem.#{key}", choice)
|
|
end
|
|
|
|
choice
|
|
end
|
|
|
|
def validate_ext_name
|
|
return unless gem_name.index("-")
|
|
|
|
Bundler.ui.error "You have specified a gem name which does not conform to the \n" \
|
|
"naming guidelines for C extensions. For more information, \n" \
|
|
"see the 'Extension Naming' section at the following URL:\n" \
|
|
"https://guides.rubygems.org/gems-with-extensions/\n"
|
|
exit 1
|
|
end
|
|
|
|
def ask_and_set_test_framework
|
|
test_framework = options[:test] || Bundler.settings["gem.test"]
|
|
|
|
if test_framework.to_s.empty?
|
|
Bundler.ui.confirm "Do you want to generate tests with your gem?"
|
|
Bundler.ui.info hint_text("test")
|
|
|
|
result = Bundler.ui.ask "Enter a test framework. rspec/minitest/test-unit/(none):"
|
|
if result =~ /rspec|minitest|test-unit/
|
|
test_framework = result
|
|
else
|
|
test_framework = false
|
|
end
|
|
end
|
|
|
|
if Bundler.settings["gem.test"].nil?
|
|
Bundler.settings.set_global("gem.test", test_framework)
|
|
end
|
|
|
|
if options[:test] == Bundler.settings["gem.test"]
|
|
Bundler.ui.info "#{options[:test]} is already configured, ignoring --test flag."
|
|
end
|
|
|
|
test_framework
|
|
end
|
|
|
|
def hint_text(setting)
|
|
if Bundler.settings["gem.#{setting}"] == false
|
|
"Your choice will only be applied to this gem."
|
|
else
|
|
"Future `bundle gem` calls will use your choice. " \
|
|
"This setting can be changed anytime with `bundle config gem.#{setting}`."
|
|
end
|
|
end
|
|
|
|
def ask_and_set_ci
|
|
ci_template = options[:ci] || Bundler.settings["gem.ci"]
|
|
|
|
if ci_template.to_s.empty?
|
|
Bundler.ui.confirm "Do you want to set up continuous integration for your gem? " \
|
|
"Supported services:\n" \
|
|
"* CircleCI: https://circleci.com/\n" \
|
|
"* GitHub Actions: https://github.com/features/actions\n" \
|
|
"* GitLab CI: https://docs.gitlab.com/ee/ci/\n" \
|
|
"* Travis CI: https://travis-ci.org/\n" \
|
|
"\n"
|
|
Bundler.ui.info hint_text("ci")
|
|
|
|
result = Bundler.ui.ask "Enter a CI service. github/travis/gitlab/circle/(none):"
|
|
if result =~ /github|travis|gitlab|circle/
|
|
ci_template = result
|
|
else
|
|
ci_template = false
|
|
end
|
|
end
|
|
|
|
if Bundler.settings["gem.ci"].nil?
|
|
Bundler.settings.set_global("gem.ci", ci_template)
|
|
end
|
|
|
|
if options[:ci] == Bundler.settings["gem.ci"]
|
|
Bundler.ui.info "#{options[:ci]} is already configured, ignoring --ci flag."
|
|
end
|
|
|
|
ci_template
|
|
end
|
|
|
|
def bundler_dependency_version
|
|
v = Gem::Version.new(Bundler::VERSION)
|
|
req = v.segments[0..1]
|
|
req << "a" if v.prerelease?
|
|
req.join(".")
|
|
end
|
|
|
|
def ensure_safe_gem_name(name, constant_array)
|
|
if name =~ /^\d/
|
|
Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers."
|
|
exit 1
|
|
end
|
|
|
|
constant_name = constant_array.join("::")
|
|
|
|
existing_constant = constant_array.inject(Object) do |c, s|
|
|
defined = begin
|
|
c.const_defined?(s)
|
|
rescue NameError
|
|
Bundler.ui.error "Invalid gem name #{name} -- `#{constant_name}` is an invalid constant name"
|
|
exit 1
|
|
end
|
|
(defined && c.const_get(s)) || break
|
|
end
|
|
|
|
return unless existing_constant
|
|
Bundler.ui.error "Invalid gem name #{name} constant #{constant_name} is already in use. Please choose another gem name."
|
|
exit 1
|
|
end
|
|
|
|
def open_editor(editor, file)
|
|
thor.run(%(#{editor} "#{file}"))
|
|
end
|
|
end
|
|
end
|