Class: Kettle::Dev::GemSpecReader
- Inherits:
-
Object
- Object
- Kettle::Dev::GemSpecReader
- Defined in:
- lib/kettle/dev/gem_spec_reader.rb
Overview
Unified gemspec reader using RubyGems loader instead of regex parsing. Returns a Hash with all data used by this project from gemspecs. Results are memoized per project root within the process.
Constant Summary collapse
- DEFAULT_MINIMUM_RUBY =
Default minimum Ruby version to assume when a gemspec doesn’t specify one.
Gem::Version.new("1.8").freeze
Class Method Summary collapse
- .clear_cache! ⇒ Object
-
.load(root) ⇒ Hash{Symbol=>Object}
Load gemspec data for the project at root using RubyGems.
Class Method Details
.clear_cache! ⇒ Object
194 195 196 |
# File 'lib/kettle/dev/gem_spec_reader.rb', line 194 def clear_cache! CACHE.mutex.synchronize { CACHE.entries.clear } end |
.load(root) ⇒ Hash{Symbol=>Object}
Load gemspec data for the project at root using RubyGems. The reader is lenient: failures to load or missing fields are handled with defaults and warnings.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/kettle/dev/gem_spec_reader.rb', line 45 def load(root) cache_key = File.(root.to_s) CACHE.mutex.synchronize do return CACHE.entries[cache_key] if CACHE.entries.key?(cache_key) end gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).first spec = nil if gemspec_path && File.file?(gemspec_path) begin spec = Gem::Specification.load(gemspec_path) rescue StandardError => e Kettle::Dev.debug_error(e, __method__) spec = nil end end gemspec_source = if gemspec_path && File.file?(gemspec_path) begin File.read(gemspec_path) rescue StandardError => e Kettle::Dev.debug_error(e, __method__) "" end else "" end gem_name = spec&.name.to_s if gem_name.nil? || gem_name.strip.empty? # Be lenient here for tasks that can proceed without gem_name (e.g., choosing destination filenames). Kernel.warn("kettle-dev: Could not derive gem name. Ensure a valid <name> is set in the gemspec.\n - Tip: set the gem name in your .gemspec file (spec.name).\n - Path searched: #{Kettle::Dev.display_path(gemspec_path || "(none found)")}") gem_name = "" end # minimum ruby version: derived from spec.required_ruby_version # Always an instance of Gem::Version min_ruby = begin # irb(main):004> Gem::Requirement.parse(spec.required_ruby_version) # => [">=", Gem::Version.new("2.3.0")] requirement = spec&.required_ruby_version if requirement tuple = Gem::Requirement.parse(requirement) tuple[1] # an instance of Gem::Version else # Default to a minimum of Ruby 1.8 puts "WARNING: Minimum Ruby not detected" DEFAULT_MINIMUM_RUBY end rescue StandardError => e puts "WARNING: Minimum Ruby detection failed:" Kettle::Dev.debug_error(e, __method__) # Default to a minimum of Ruby 1.8 DEFAULT_MINIMUM_RUBY end homepage_val = spec&.homepage.to_s explicit_forge_org = env_org_override("FORGE_ORG") # Derive org/repo from homepage or git remote forge_info = derive_forge_and_origin_repo(homepage_val) forge_org = explicit_forge_org || forge_info[:forge_org] gh_repo = forge_info[:origin_repo] if forge_org.to_s.empty? Kernel.warn("kettle-dev: Could not determine forge org from spec.homepage or git remote.\n - Ensure gemspec.homepage is set to a GitHub URL or that the git remote 'origin' points to GitHub.\n - Example homepage: https://github.com/<org>/<repo>\n - Proceeding with default org: kettle-rb.") forge_org = "kettle-rb" end camel = lambda do |s| s.to_s.split(/[_-]/).map { |p| p.gsub(/\b([a-z])/) { Regexp.last_match(1).upcase } }.join end entrypoint_require = derive_entrypoint_require( root: root, gem_name: gem_name, gemspec_source: gemspec_source, ) namespace_source = entrypoint_require.to_s.empty? ? gem_name.to_s.tr("-", "/") : entrypoint_require.to_s namespace = namespace_source.split("/").reject(&:empty?).map { |seg| camel.call(seg) }.join("::") namespace_shield = namespace.gsub("::", "%3A%3A") gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__") # Funding org (Open Collective handle) detection. # Precedence: # 1) OpenCollectiveConfig.disabled? - when true, funding_org is nil # 2) ENV["FUNDING_ORG"] when set and non-empty (unless already disabled above) # 3) OpenCollectiveConfig.handle(required: false) # Be lenient: allow nil when not discoverable, with a concise warning. begin # Check if Open Collective is explicitly disabled via environment variables if OpenCollectiveConfig.disabled? funding_org = nil else env_funding = ENV["FUNDING_ORG"] if env_funding && !env_funding.to_s.strip.empty? && !env_funding.match?(/\{KJ\|[^}]+}/) # FUNDING_ORG is set, non-empty, and is not an unresolved token placeholder; # use it as-is (already filtered by opencollective_disabled?) funding_org = env_funding.to_s else # Preflight: if a YAML exists under the provided root, attempt to read it here so # unexpected file IO errors surface within this rescue block (see specs). oc_path = OpenCollectiveConfig.yaml_path(root) File.read(oc_path) if File.file?(oc_path) funding_org = OpenCollectiveConfig.handle(required: false, root: root) if funding_org.to_s.strip.empty? Kernel.warn("kettle-dev: Could not determine funding org.\n - Options:\n * Set ENV['FUNDING_ORG'] to your funding handle, or 'false' to disable.\n * Or set ENV['OPENCOLLECTIVE_HANDLE'].\n * Or add .opencollective.yml with: collective: <handle> (or org: <handle>).\n * Or proceed without funding if not applicable.") funding_org = nil end end end rescue StandardError => error Kettle::Dev.debug_error(error, __method__) # In an unexpected exception path, escalate to a domain error to aid callers/specs raise Kettle::Dev::Error, "Unable to determine funding org: #{error.}" end result = { gemspec_path: gemspec_path, gem_name: gem_name, version: spec&.version.to_s, min_ruby: min_ruby, # Gem::Version instance homepage: homepage_val.to_s, gh_org: forge_org, # Might allow divergence from forge_org someday forge_org: forge_org, funding_org: funding_org, gh_repo: gh_repo, namespace: namespace, namespace_shield: namespace_shield, entrypoint_require: entrypoint_require, gem_shield: gem_shield, # Additional fields sourced from the gemspec for templating carry-over authors: Array(spec&.).compact.uniq, email: Array(spec&.email).compact.uniq, summary: spec&.summary.to_s, description: spec&.description.to_s, licenses: Array(spec&.licenses), # licenses will include any specified as license (singular) required_ruby_version: spec&.required_ruby_version, # Gem::Requirement instance require_paths: Array(spec&.require_paths), bindir: (spec&.bindir || "").to_s, executables: Array(spec&.executables), } CACHE.mutex.synchronize do CACHE.entries[cache_key] = result end result end |