|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require "json" |
| 4 | +require "open3" |
| 5 | + |
| 6 | +namespace :pshopify do |
| 7 | + desc "Create a new pshopify definition. Usage: rake \"pshopify:create[4.0.2]\" (defaults to latest stable)" |
| 8 | + task :create, [:base_version] do |_t, args| |
| 9 | + creator = PshopifyDefinitionCreator.new(args[:base_version]) |
| 10 | + creator.run |
| 11 | + end |
| 12 | +end |
| 13 | + |
| 14 | +class PshopifyDefinitionCreator |
| 15 | + SHOPIFY_RUBY_REPO = "Shopify/ruby" |
| 16 | + SHOPIFY_RUBY_GIT_URL = "https://github.com/#{SHOPIFY_RUBY_REPO}.git" |
| 17 | + RUBIES_DIR = File.expand_path("../../rubies", __dir__) |
| 18 | + |
| 19 | + def initialize(base_version = nil) |
| 20 | + @base_version = base_version |
| 21 | + end |
| 22 | + |
| 23 | + def run |
| 24 | + @ruby_build_defs_dir = find_ruby_build_defs_dir |
| 25 | + @base_version ||= detect_latest_stable |
| 26 | + @pshopify_num = next_pshopify_number |
| 27 | + @pshopify_version = "#{@base_version}-pshopify#{@pshopify_num}" |
| 28 | + @tag = "v#{@pshopify_version}" |
| 29 | + |
| 30 | + puts "Creating definition for #{@pshopify_version}..." |
| 31 | + |
| 32 | + verify_branch_exists |
| 33 | + openssl_line = read_upstream_openssl_line |
| 34 | + compare_base = determine_compare_base |
| 35 | + verify_previous_pshopify_included if @pshopify_num > 1 |
| 36 | + commits = fetch_changelog(compare_base) |
| 37 | + |
| 38 | + content = generate_definition(compare_base, commits, openssl_line) |
| 39 | + output_path = File.join(RUBIES_DIR, @pshopify_version) |
| 40 | + File.write(output_path, content) |
| 41 | + |
| 42 | + puts "Created #{output_path}" |
| 43 | + end |
| 44 | + |
| 45 | + private |
| 46 | + |
| 47 | + def next_pshopify_number |
| 48 | + existing = Dir.children(RUBIES_DIR) |
| 49 | + .filter_map { |f| f[/\A#{Regexp.escape(@base_version)}-pshopify(\d+)\z/, 1]&.to_i } |
| 50 | + |
| 51 | + (existing.max || 0) + 1 |
| 52 | + end |
| 53 | + |
| 54 | + def verify_branch_exists |
| 55 | + gh_api("repos/#{SHOPIFY_RUBY_REPO}/branches/#{@tag}") |
| 56 | + puts " Branch #{@tag} exists" |
| 57 | + rescue GhApiError => e |
| 58 | + abort("Error: branch #{@tag} does not exist on #{SHOPIFY_RUBY_REPO}\n#{e.message}") |
| 59 | + end |
| 60 | + |
| 61 | + def read_upstream_openssl_line |
| 62 | + definition_path = File.join(@ruby_build_defs_dir, @base_version) |
| 63 | + |
| 64 | + unless File.exist?(definition_path) |
| 65 | + abort("Error: upstream definition for #{@base_version} not found in #{@ruby_build_defs_dir}") |
| 66 | + end |
| 67 | + |
| 68 | + line = File.readlines(definition_path).find { |l| l.include?("openssl") } |
| 69 | + abort("Error: no openssl line found in #{definition_path}") unless line |
| 70 | + |
| 71 | + line.chomp |
| 72 | + end |
| 73 | + |
| 74 | + def find_ruby_build_defs_dir |
| 75 | + candidates = if ENV["RUBY_BUILD_PATH"] |
| 76 | + [ENV["RUBY_BUILD_PATH"]] |
| 77 | + else |
| 78 | + parent = File.dirname(RUBIES_DIR, 2) |
| 79 | + Dir.glob(File.join(parent, "*ruby-build")) |
| 80 | + end |
| 81 | + |
| 82 | + candidates.each do |dir| |
| 83 | + defs_dir = File.join(dir, "share", "ruby-build") |
| 84 | + return defs_dir if Dir.exist?(defs_dir) |
| 85 | + end |
| 86 | + |
| 87 | + abort("Error: ruby-build definitions not found.\n" \ |
| 88 | + "Set RUBY_BUILD_PATH to your ruby-build checkout.") |
| 89 | + end |
| 90 | + |
| 91 | + def detect_latest_stable |
| 92 | + versions = Dir.children(@ruby_build_defs_dir) |
| 93 | + .filter_map { |f| f if f.match?(/\A\d+\.\d+\.\d+\z/) } |
| 94 | + .sort_by { |v| Gem::Version.new(v) } |
| 95 | + |
| 96 | + abort("Error: no stable Ruby versions found in #{@ruby_build_defs_dir}") if versions.empty? |
| 97 | + |
| 98 | + latest = versions.last |
| 99 | + puts "No version specified, using latest stable: #{latest}" |
| 100 | + latest |
| 101 | + end |
| 102 | + |
| 103 | + def determine_compare_base |
| 104 | + if @pshopify_num == 1 |
| 105 | + resolve_upstream_tag |
| 106 | + else |
| 107 | + "v#{@base_version}-pshopify#{@pshopify_num - 1}" |
| 108 | + end |
| 109 | + end |
| 110 | + |
| 111 | + # Upstream tags may use dots (v4.0.2) or underscores (v3_4_8). |
| 112 | + def resolve_upstream_tag |
| 113 | + tag_with_dots = "v#{@base_version}" |
| 114 | + tag_with_underscores = "v#{@base_version.tr(".", "_")}" |
| 115 | + |
| 116 | + [tag_with_dots, tag_with_underscores].each do |tag| |
| 117 | + gh_api("repos/#{SHOPIFY_RUBY_REPO}/git/ref/tags/#{tag}") |
| 118 | + return tag |
| 119 | + rescue GhApiError |
| 120 | + next |
| 121 | + end |
| 122 | + |
| 123 | + # Fall back to the dotted form for the compare URL even if we can't confirm it |
| 124 | + warn("Warning: could not confirm upstream tag format, using #{tag_with_dots}") |
| 125 | + tag_with_dots |
| 126 | + end |
| 127 | + |
| 128 | + def verify_previous_pshopify_included |
| 129 | + prev_tag = "v#{@base_version}-pshopify#{@pshopify_num - 1}" |
| 130 | + data = gh_api("repos/#{SHOPIFY_RUBY_REPO}/compare/#{prev_tag}...#{@tag}") |
| 131 | + |
| 132 | + status = data["status"] |
| 133 | + if status == "behind" || status == "diverged" |
| 134 | + warn("WARNING: #{@tag} is #{status} relative to #{prev_tag}.") |
| 135 | + warn(" The new branch may be missing commits from the previous pshopify.") |
| 136 | + warn(" Ahead: #{data["ahead_by"]}, Behind: #{data["behind_by"]}") |
| 137 | + else |
| 138 | + puts " #{@tag} includes all commits from #{prev_tag}" |
| 139 | + end |
| 140 | + rescue GhApiError => e |
| 141 | + warn("Warning: could not compare with previous pshopify: #{e.message}") |
| 142 | + end |
| 143 | + |
| 144 | + def fetch_changelog(compare_base) |
| 145 | + data = gh_api("repos/#{SHOPIFY_RUBY_REPO}/compare/#{compare_base}...#{@tag}") |
| 146 | + commits = data.fetch("commits", []) |
| 147 | + commits.map { |c| c.dig("commit", "message").lines.first.chomp } |
| 148 | + rescue GhApiError => e |
| 149 | + warn("Warning: could not fetch changelog: #{e.message}") |
| 150 | + [] |
| 151 | + end |
| 152 | + |
| 153 | + def generate_definition(compare_base, commits, openssl_line) |
| 154 | + lines = [] |
| 155 | + lines << "# https://github.com/#{SHOPIFY_RUBY_REPO}/compare/#{compare_base}...Shopify:#{@tag}" |
| 156 | + lines << "" |
| 157 | + |
| 158 | + if commits.any? |
| 159 | + lines << "# Based off #{compare_base}, with the following changes:" |
| 160 | + commits.each { |msg| lines << "# * #{msg}" } |
| 161 | + else |
| 162 | + lines << "# Based off #{compare_base}" |
| 163 | + end |
| 164 | + |
| 165 | + lines << "" |
| 166 | + lines << openssl_line |
| 167 | + lines << "install_git \"ruby-#{@pshopify_version}\" \"#{SHOPIFY_RUBY_GIT_URL}\" \"#{@tag}\"" \ |
| 168 | + "autoconf enable_shared standard" |
| 169 | + lines << "" |
| 170 | + |
| 171 | + lines.join("\n") |
| 172 | + end |
| 173 | + |
| 174 | + class GhApiError < StandardError; end |
| 175 | + |
| 176 | + def gh_api(endpoint) |
| 177 | + out, err, status = Open3.capture3("gh", "api", endpoint) |
| 178 | + raise GhApiError, "gh api #{endpoint} failed: #{err.strip}" unless status.success? |
| 179 | + |
| 180 | + JSON.parse(out) |
| 181 | + end |
| 182 | +end |
0 commit comments