diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 7b3581f796..f84dacbf75 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -11,25 +11,35 @@ jobs: if: github.repository_owner == 'Homebrew' || !github.event.schedule runs-on: ubuntu-latest steps: + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + with: + core: false + cask: false + test-bot: true + - uses: actions/checkout@v4 with: persist-credentials: false - - name: Set up Ruby 3 - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true + + - name: Install Homebrew Bundler RubyGems + if: steps.cache.outputs.cache-hit != 'true' + run: brew install-bundler-gems + - name: Configure git run: | git config user.email 'ta2gch@gmail.com' git config user.name 'TANIGUCHI Masaya' - - name: Convert and commit - run: | - git submodule update --init --remote - bundle exec ruby cask2formula convert - bundle exec ruby cask2formula commit - - name: Clean Formula directory - run: | - comm -23 --nocheck-order <(ls -1 Formula) <(basename -a homebrew-cask-fonts/Casks/font/*/*) | xargs -I{} sh -c 'git rm ./Formula/{} && git commit -m "Remove {}"' + + - name: Generate formulae + if: github.event_name == 'pull_request' + run: brew generate-linux-fonts --verbose --debug --write-only + + - name: Generate and commit formulae + if: github.event_name != 'pull_request' + run: brew generate-linux-fonts --verbose --debug + - name: Publish to GitHub if: github.event_name != 'pull_request' env: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 44c6d0e502..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "homebrew-cask-fonts"] - path = homebrew-cask-fonts - url = https://github.com/homebrew/homebrew-cask - shallow = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e5c2ac13b..3e90ba5a9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,35 +12,35 @@ Making a Font Cask is easy: a Cask is a small Ruby file. Here’s a Cask for the font [Inconsolata](http://levien.com/type/myfonts/inconsolata.html) as an example: ```ruby -cask 'font-inconsolata' do +cask "font-inconsolata" do version :latest sha256 :no_check - url 'http://levien.com/type/myfonts/Inconsolata.otf' - name 'Inconsolata' - homepage 'http://levien.com/type/myfonts/inconsolata.html' + url "http://levien.com/type/myfonts/Inconsolata.otf" + name "Inconsolata" + homepage "http://levien.com/type/myfonts/inconsolata.html" - font 'Inconsolata.otf' + font "Inconsolata.otf" end ``` Here’s a more complex Cask for the font [Fantasque Sans Mono](https://github.com/belluzj/fantasque-sans). Note that you may repeat the `font` stanza as many times as you need to, if multiple files must be installed from the same package: ```ruby -cask 'font-fantasque-sans-mono' do - version '1.7.1' - sha256 '6bb3b24413b78eed19ffa9bd233ae555982e3b185bd303e57dd1e05bebf17352' +cask "font-fantasque-sans-mono" do + version "1.7.1" + sha256 "6bb3b24413b78eed19ffa9bd233ae555982e3b185bd303e57dd1e05bebf17352" url "https://github.com/belluzj/fantasque-sans/releases/download/v#{version}/FantasqueSansMono.zip" - appcast 'https://github.com/belluzj/fantasque-sans/releases.atom', - checkpoint: '8085c3dff43a9dbf3201ca790c57800a089d1b69fec91226a600c04d9c681e36' - name 'Fantasque Sans Mono' - homepage 'https://github.com/belluzj/fantasque-sans' - - font 'OTF/FantasqueSansMono-Bold.otf' - font 'OTF/FantasqueSansMono-BoldItalic.otf' - font 'OTF/FantasqueSansMono-Italic.otf' - font 'OTF/FantasqueSansMono-Regular.otf' + appcast "https://github.com/belluzj/fantasque-sans/releases.atom", + checkpoint: "8085c3dff43a9dbf3201ca790c57800a089d1b69fec91226a600c04d9c681e36" + name "Fantasque Sans Mono" + homepage "https://github.com/belluzj/fantasque-sans" + + font "OTF/FantasqueSansMono-Bold.otf" + font "OTF/FantasqueSansMono-BoldItalic.otf" + font "OTF/FantasqueSansMono-Italic.otf" + font "OTF/FantasqueSansMono-Regular.otf" end ``` diff --git a/Formula/.gitkeep b/Formula/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 5387e87d0b..0000000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true -source "https://rubygems.org" - -ruby file: ".ruby-version" - -gem "parslet" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 0840e436b5..0000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - parslet (1.8.2) - -PLATFORMS - ruby - -DEPENDENCIES - parslet - -RUBY VERSION - ruby 3.3.1p55 - -BUNDLED WITH - 2.5.9 diff --git a/cask2formula b/cask2formula deleted file mode 100755 index 1c5132fb5c..0000000000 --- a/cask2formula +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env ruby - -require "parslet" - -def replace_cask_version_properties(input_string) - input_string - .gsub(".no_dots", '.gsub(".", "")') - .gsub(".no_hyphens", '.gsub("-", "")') - .gsub(".dots_to_underscores", '.gsub(".", "_")') - .gsub(".dots_to_slashes", '.gsub(".", "/")') - .gsub(".dots_to_hyphens", '.gsub(".", "-")') - .gsub(".major", '.sub(/\\..*/, "")') - .gsub(".minor", '.sub(/.*\\./, "")') - .gsub(".before_comma", '.sub(/,.*/, "")') - .gsub(".after_comma", '.sub(/.*,/, "")') - .gsub(".csv.first", '.sub(/,.*/, "")') - .gsub(".csv.second", '.sub(/.*,/, "")') - .gsub("version.", "version.to_s.") -end - -class CaskParser < Parslet::Parser - rule(:space) { - match("[ \t\n\r]").repeat(1) - } - rule(:string) { - ((str('"') >> - ((str('#{') >> (str('}').absent? >> any).repeat >> str('}')) | - (str('"').absent? >> any)).repeat >> - str('"')) | - (str("'") >> - (str("'").absent? >> any).repeat >> - str("'"))).as(:string) - } - rule(:word) { - match("[0-9a-zA-Z_]").repeat(1) - } - rule(:keyword) { - (str(":") >> word).as(:keyword) - } - rule(:pair) { - (word >> str(":")).as(:left) >> space >> (string | keyword | word).as(:right) - } - rule(:doblock) { - (str("do") >> space >> - ((space >> doblock) | (match("[ \t\n\r]+end").absent? >> any)).repeat >> - space >> str("end")) - } - rule(:heredoc) { - (str("<<~EOS") >> (str("EOS").absent? >> any).repeat >> str("EOS")).as(:heredoc) - } - rule(:value) { - (string | keyword | pair | heredoc | doblock.as(:doblock)) - } - rule(:font) { - str("font") >> space >> string.as(:font) - } - rule(:command) { - (str("font").absent? >> word >> str("!").maybe).as(:command) >> space >> - value.as(:first_argument) >> - (str(",") >> space >> value).repeat.as(:rest_arguments) - } - rule(:comment) { - (str("#") >> (str("\n").absent? >> any).repeat).as(:comment) - } - rule(:cask) { - str("cask") >> space >> string.as(:name) >> space >> str("do") >> space >> - ((command | comment) >> space).repeat.as(:before) >> - (font >> space).repeat.as(:fonts) >> - ((command | comment) >> space).repeat.as(:after) >> - str("end") >> str("\n") - } - root :cask -end - -$my_latest = false -$my_no_check = false -$my_only_path = "" - -class CaskTransform < Parslet::Transform - rule(:string => simple(:string)) { - string.to_s.sub(/^'([^"]*)'$/) { '"'+$1+'"' } - } - rule(:keyword => simple(:keyword)) { - keyword.to_s - } - rule(:heredoc => simple(:heredoc)) { - heredoc.to_s - } - rule(:doblock => simple(:doblock)) { - doblock.to_s - } - rule(:left => simple(:left), :right => simple(:right) ) { - left + " " + right - } - rule(:comment => simple(:comment)) { - comment.to_s + "\n" - } - rule(:font => simple(:font)) { - afont = font.to_s.sub(/^"([^"]*)"$/, '\1') - dirname = File.dirname(afont) - basename = File.basename(afont) - afont = File.join(dirname, "**", basename) - if $my_only_path then - afont = File.join($my_only_path, afont) - end - afont = replace_cask_version_properties(afont) - afont = afont.sub(/\[/, '\\\\\\\\[').sub(/\]/, '\\\\\\\\]') - "(share/\"fonts\").install Dir.glob(\"#{afont}\")[0]" - } - rule(:name => simple(:name), - :before => sequence(:before), - :fonts => sequence(:fonts), - :after => sequence(:after)) { - commands1 = before.select{|c| c != ""} - camelcase = name.to_s.gsub('"', "").gsub(/(^|-)(\w)/) { $2.upcase } - commands2 = after.select{|c| c != ""}+["test do\n end"] - <<~EOS - class #{camelcase} < Formula - #{commands1.join("\n ")} - def install - #{fonts.join("\n ")} - end - #{commands2.join("\n ")} - end - EOS - } - rule(:command => simple(:command), - :first_argument => simple(:first_argument), - :rest_arguments => sequence(:rest_arguments)) { - arguments = rest_arguments.unshift(first_argument) - if command == "caveats" then - "def creavat; #{arguments.join(", ")}\n end" - elsif command == "name" then - "desc #{arguments.join(", ")}" - elsif command == "depends_on" then - # Delete depends_on macos ... - # "depends_on #{arguments.join(",").gsub("formula: ","")}" - "" - elsif command == "appcast" || command == "container" then #TODO: "container type:"" - "" - elsif command == "sha256" && arguments.join(", ").include?(":no_check") then - $my_no_check = true - "" - elsif command == "version" && arguments.join(", ").include?(":latest") then - $my_latest = true - "" - elsif command == "url" && $my_latest && $my_no_check then - if arguments.join(", ").include? "only_path" then - $my_only_path = arguments - .find{ |arg| arg.to_s.include? "only_path" } - .to_s - .gsub(/only_path:\s*"(.*)"/, '\1') - end - "head #{arguments.join(", ")}" - elsif command == "livecheck" then - "" - elsif command == "deprecate!" || command == "disable!" then - command + " " + arguments.join(", ").gsub(":discontinued", ":unsupported") - else - command + " " + replace_cask_version_properties(arguments.join(", ").to_s) - end - } -end - -class CaskConverter - def convert - FileUtils.mkdir_p("Formula") - ignores = IO.readlines('.caskignore', chomp: true) - transform = CaskTransform.new(true) - parser = CaskParser.new - Dir.glob('./homebrew-cask-fonts/Casks/font/*/*.rb').select{|file| - ignores.all? { |ignore| !File.fnmatch(ignore, file) } - }.each do |cask| - p "< #{cask}" - recipe = File.read(cask, encoding: 'UTF-8') - $my_latest = false - $my_no_check = false - $my_only_path = nil - recipe = parser.parse(recipe) - recipe = transform.apply(recipe) - formula = cask.sub(%r{homebrew-cask-fonts/Casks/font/font-[0-9a-zA-Z]/}, 'Formula/') - File.write(formula, recipe) - rescue Parslet::ParseFailed => failure - $stderr.puts failure.parse_failure_cause.ascii_tree - exit 1 - end - end - - def commit - `git diff --name-only; git ls-files --others --exclude-standard`.split(/\s+/).each do |file| - if file != "homebrew-cask-fonts" then - shard_letter = File.basename(file).delete_prefix("font-")[0] - remote_file = file.sub(/Formula/, "Casks/font/font-#{shard_letter}") - remote_source = "https://github.com/Homebrew/homebrew-cask/blob/master/#{remote_file}" - commit_id = `git -C homebrew-cask-fonts log -n 1 --pretty=format:%H -- #{remote_file}` - system 'git', 'add', file - system 'git', 'commit', '-m', "import #{remote_source} from #{commit_id}" - end - end - system 'git', 'add', 'homebrew-cask-fonts' - system 'git', 'commit', '-m', 'Update submodule' - end -end - -Dir.chdir(__dir__) -converter = CaskConverter.new -case ARGV[0] -when "convert" then converter.convert -when "commit" then converter.commit -else p "ERROR!" -end diff --git a/cmd/generate-linux-fonts.rb b/cmd/generate-linux-fonts.rb new file mode 100755 index 0000000000..ed298f4d84 --- /dev/null +++ b/cmd/generate-linux-fonts.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +require "cli/parser" +require "pathname" +require "open-uri" +require "json" +require "tempfile" + +module Homebrew + module_function + + sig { returns(CLI::Parser) } + def generate_linux_fonts_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `generate-linux-fonts` + + Generate formulae from font casks for use on linux + EOS + + switch "--write-only", description: "Do not commit changed files." + switch "--keep-deleted", description: "Do not remove fonts that were removed as a cask." + + hide_from_man_page! + end + end + + def generate_linux_fonts + args = generate_linux_fonts_args.parse + + output_dir = Pathname.new("Formula") + remaining_fonts = Utils.safe_popen_read("ls", output_dir.to_s).strip.split + download_cask_data.each do |cask| + next unless /^font-/.match?(cask["token"]) + + odebug "Creating #{cask["token"]} in #{output_dir/"#{cask["token"]}.rb"}" + (output_dir/"#{cask["token"]}.rb").write generate_formula(cask) + remaining_fonts.delete("#{cask["token"]}.rb") + end + + return if args.write_only? + + remaining_fonts.each do |remaining_font| + (output_dir/remaining_font).unlink unless args.keep_deleted? + end + + create_commits(:A, output_dir) + create_commits(:M, output_dir) + create_commits(:D, output_dir) + end + + sig { returns(T::Hash[String, String]) } + def download_cask_data + file = Tempfile.new("cask.json") + download = URI.open("https://formulae.brew.sh/api/cask.json") + IO.copy_stream(download, file.path) + JSON.parse(file.read) + ensure + file.close + file.unlink + end + + def create_commits(type, output_dir) + files = Utils.safe_popen_read("git", "diff", "--name-only", "--diff-filter=#{type}", + output_dir.to_s).strip.split("\n") + if type == :A + files += Utils.safe_popen_read("git", "ls-files", "--others", "--exclude-standard").strip.split("\n") + end + + odebug files + files.each do |file| + token = file.gsub("#{output_dir}/", "").gsub(".rb", "") + message = case type + when :A + "#{token} (new font)" + when :M + "#{token} updated" + when :D + "#{token} deleted" + end + + odebug "type (#{type}) for #{file}" + odebug Utils.safe_popen_read("git", "add", file) + odebug Utils.safe_popen_read("git", "commit", "-m", message) + end + end + + sig { params(stanza: Symbol, value: T.untyped, args: T::Hash[String, String]).returns(String) } + def format_stanza(stanza, value, args = {}) + return "" if value.nil? + + if stanza == :sha256 && value == "no_check" + <<-EOS + #{stanza} :#{value} + EOS + elsif [:url, :head].include?(stanza) + <<-EOS + #{stanza} #{format_url(value, args)} + EOS + else + <<-EOS + #{stanza} "#{value.gsub("\"", "\\\"")}" + EOS + end + end + + def format_url(value, args) + ret = "\"#{value}\"" + + return ret unless args.present? + + longest_key = args.max_by { |k, _| k.length }&.first + args.each do |key, arg_val| + key_s = "#{key}:" + ret += ",\n #{key_s.ljust((longest_key.length + 1), " ")} \"#{arg_val}\"" + end + + ret + end + + def get_font_paths(cask) + paths = [] + + path_prefix = if cask["url_specs"].key?("only_path") + "#{cask["url_specs"]["only_path"]}/" + else + "" + end + + cask["artifacts"].each do |artifact| + next unless artifact.key?("font") + + artifact["font"].each do |font| + paths << "#{path_prefix}./**/#{font}" + end + end + + paths + end + + def generate_formula(cask) + classname = cask["token"].split("-").map(&:capitalize).join + formula = <<~EOS + class #{classname} < Formula + EOS + + formula += format_stanza :desc, cask["desc"] || "#{cask["name"].first&.capitalize} font" + formula += format_stanza :homepage, cask["homepage"] + + if cask["version"] != "latest" + formula += format_stanza :url, cask["url"], cask["url_specs"] + formula += format_stanza :version, cask["version"] + formula += format_stanza :sha256, cask["sha256"] + end + + formula += format_stanza :head, cask["url"], cask["url_specs"] if cask["version"] == "latest" + + paths = get_font_paths(cask) + if paths.count < 1 + formula += <<-EOS + + depends_on :macos + EOS + end + + if cask["disable_date"].present? + formula += <<-EOS + + disable! "#{cask["disable_date"]}", because: :#{cask["disable_reason"]} + EOS + elsif cask["deprecation_date"].present? + formula += <<-EOS + + deprecate! \"#{cask["deprecation_date"]}\", because: :#{cask["deprecation_reason"]} + EOS + end + + formula += <<-EOS + + def install + EOS + + paths.each do |path| + formula += <<-EOS + (share/\"fonts\").install Dir.glob("#{path}")[0] + EOS + end + + if paths.count < 1 + formula += <<-EOS + # nothing to install + EOS + end + + formula += <<~EOS + end + + test do + assert_path_exists share/"fonts" + end + end + EOS + + formula + end +end diff --git a/homebrew-cask-fonts b/homebrew-cask-fonts deleted file mode 160000 index f32bf43913..0000000000 --- a/homebrew-cask-fonts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f32bf439136639c51823af387705f915390484f5