ratatui_ruby-devtools 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +53 -7
- data/README.md +49 -21
- data/Rakefile +1 -1
- data/bin/setup +20 -6
- data/bin/setup.ps1 +74 -0
- data/doc/contributors/documentation_style.md +3 -4
- data/exe/scaffold +8 -342
- data/lib/ratatui_ruby/devtools/scaffold/scaffold_config.rb +67 -0
- data/lib/ratatui_ruby/devtools/scaffold/template_renderer.rb +282 -0
- data/lib/ratatui_ruby/devtools/task_scope.rb +24 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc_tasks.rb +31 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/bump_workflow.rb +71 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +60 -3
- data/lib/ratatui_ruby/devtools/tasks/bump/github_project.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +3 -3
- data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +39 -24
- data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +9 -17
- data/lib/ratatui_ruby/devtools/tasks/bump/no_crate.rb +19 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/no_links.rb +22 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/patch_release.rb +25 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/release_branch.rb +17 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/release_from_trunk.rb +48 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/repository.rb +143 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +65 -59
- data/lib/ratatui_ruby/devtools/tasks/bump/rust_crate.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +127 -10
- data/lib/ratatui_ruby/devtools/tasks/bump/source_hut_project.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unbranched_release.rb +20 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +19 -1
- data/lib/ratatui_ruby/devtools/tasks/bump_tasks.rb +47 -0
- data/lib/ratatui_ruby/devtools/tasks/cargo_tasks.rb +57 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/documentation.rb +59 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/file_url.rb +30 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/relative_path.rb +61 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/web_url.rb +55 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link.rb +52 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link_audit.rb +116 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/problem.rb +40 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/source_file.rb +93 -0
- data/lib/ratatui_ruby/devtools/tasks/doc_tasks.rb +937 -0
- data/lib/ratatui_ruby/devtools/tasks/license/blame.rb +72 -0
- data/lib/ratatui_ruby/devtools/tasks/license/changed_files.rb +34 -0
- data/lib/ratatui_ruby/devtools/tasks/license/contribution.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/copyright.rb +47 -0
- data/lib/ratatui_ruby/devtools/tasks/license/directory_licensing.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/license/existing_header.rb +21 -0
- data/lib/ratatui_ruby/devtools/tasks/license/header_revision.rb +45 -0
- data/lib/ratatui_ruby/devtools/tasks/license/licensed_file.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/licensing.rb +35 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_document.rb +23 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_files.rb +31 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_header_syntax.rb +104 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_snippet_syntax.rb +159 -0
- data/lib/ratatui_ruby/devtools/tasks/license/missing_header.rb +18 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rbs_files.rb +27 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rbs_header_syntax.rb +31 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_comment.rb +51 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_examples.rb +41 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_snippet_syntax.rb +119 -0
- data/lib/ratatui_ruby/devtools/tasks/license/renewal.rb +17 -0
- data/lib/ratatui_ruby/devtools/tasks/license/reuse_annotated_file.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/reuse_tool.rb +34 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_comment_header.rb +49 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_files.rb +36 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_header_syntax.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_files.rb +27 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_header_syntax.rb +59 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet.rb +44 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_file.rb +29 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_opening.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_revision.rb +75 -0
- data/lib/ratatui_ruby/devtools/tasks/license/spdx_header.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/license/unknown_file.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/unlicensed_files.rb +69 -0
- data/lib/ratatui_ruby/devtools/tasks/license/wrapped_snippet.rb +30 -0
- data/lib/ratatui_ruby/devtools/tasks/license_tasks.rb +71 -0
- data/lib/ratatui_ruby/devtools/tasks/lint/gem_lockfile.rb +29 -0
- data/lib/ratatui_ruby/devtools/tasks/lint/supported_rubies.rb +18 -0
- data/lib/ratatui_ruby/devtools/tasks/lint_tasks.rb +100 -0
- data/lib/ratatui_ruby/devtools/tasks/release/platform_gem.rb +21 -12
- data/lib/ratatui_ruby/devtools/tasks/release/versioned_binary.rb +1 -1
- data/lib/ratatui_ruby/devtools/tasks/release_tasks.rb +80 -0
- data/lib/ratatui_ruby/devtools/tasks/reuse_tasks.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/{test.rake → test_tasks.rb} +13 -4
- data/lib/ratatui_ruby/devtools/tasks/website/hosted_source_tree.rb +24 -0
- data/lib/ratatui_ruby/devtools/tasks/website/index_page.rb +1 -4
- data/lib/ratatui_ruby/devtools/tasks/website/source_tree.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/website/versioned_documentation.rb +14 -13
- data/lib/ratatui_ruby/devtools/tasks/website_tasks.rb +49 -0
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/build-gems.yml.erb +7 -1
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/ci.yml.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +2 -1
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +3 -1
- data/lib/ratatui_ruby/devtools/templates/CONTRIBUTING.md.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +2 -3
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +7 -7
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +5 -1
- data/lib/ratatui_ruby/devtools/templates/bin/setup.ps1.erb +5 -1
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +4 -4
- data/lib/ratatui_ruby/devtools/templates/ext/Cargo.toml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +3 -3
- data/lib/ratatui_ruby/devtools/version.rb +1 -1
- data/lib/ratatui_ruby/devtools.rb +54 -71
- data/mise.toml +2 -2
- data/sig/ratatui_ruby/devtools.rbs +16 -1
- metadata +93 -34
- data/.builds/ruby-4.0.yml +0 -38
- data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +0 -21
- data/lib/ratatui_ruby/devtools/tasks/bump.rake +0 -80
- data/lib/ratatui_ruby/devtools/tasks/cargo.rake +0 -47
- data/lib/ratatui_ruby/devtools/tasks/doc.rake +0 -887
- data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +0 -276
- data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +0 -236
- data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +0 -143
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +0 -353
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +0 -186
- data/lib/ratatui_ruby/devtools/tasks/license.rake +0 -91
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +0 -80
- data/lib/ratatui_ruby/devtools/tasks/release.rake +0 -87
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +0 -7
- data/lib/ratatui_ruby/devtools/tasks/reuse.rake +0 -104
- data/lib/ratatui_ruby/devtools/tasks/website.rake +0 -37
- data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +0 -47
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +0 -45
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +0 -8
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "blame"
|
|
9
|
+
require_relative "copyright"
|
|
10
|
+
|
|
11
|
+
# One file's snippets brought up to date: each bare snippet asks to be
|
|
12
|
+
# wrapped, and each wrapped one whose notices have moved on asks to be
|
|
13
|
+
# reopened.
|
|
14
|
+
class SnippetRevision # :nodoc:
|
|
15
|
+
def initialize(path:, text:, syntax:, author:)
|
|
16
|
+
@path = path
|
|
17
|
+
@original = text
|
|
18
|
+
@syntax = syntax
|
|
19
|
+
@author = author
|
|
20
|
+
@lines = syntax.cleaned(text.lines)
|
|
21
|
+
@snippets = syntax.snippets_in(@lines)
|
|
22
|
+
@notices = []
|
|
23
|
+
# Blame reads the cleaned lines the snippets were found in, not the file
|
|
24
|
+
# on disk.
|
|
25
|
+
@blame = Blame.new(path:, contents: @lines.join)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Revises from the last snippet back, so the earlier ones stay on the lines
|
|
29
|
+
# blame found them on.
|
|
30
|
+
def run
|
|
31
|
+
@snippets.reverse_each { |snippet| snippet.revise(self) }
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def text
|
|
36
|
+
@lines.join
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def changed?
|
|
40
|
+
text != @original
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# What the revision did, for whoever ran it.
|
|
44
|
+
def messages
|
|
45
|
+
return [] unless changed?
|
|
46
|
+
|
|
47
|
+
wrapped = @snippets.count(&:bare?)
|
|
48
|
+
summary = if wrapped.zero?
|
|
49
|
+
"Updated: #{@path}"
|
|
50
|
+
else
|
|
51
|
+
"Updated: #{@path} (#{wrapped} code block(s))"
|
|
52
|
+
end
|
|
53
|
+
[*@notices.map { |notice| " #{notice}" }, summary]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def contributions(snippet)
|
|
57
|
+
@blame.contributions([snippet.blame_range], uncommitted_by: @author)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def license
|
|
61
|
+
@syntax.license
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def wrap(snippet)
|
|
65
|
+
copyrights = contributions(snippet).map { |contribution| Copyright.of(contribution) }.sort_by(&:holder)
|
|
66
|
+
@lines.insert(snippet.last + 1, *@syntax.closing(snippet))
|
|
67
|
+
@lines.insert(snippet.first, *@syntax.opening(snippet, copyrights))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reopen(snippet, renewal)
|
|
71
|
+
opening = renewal.value
|
|
72
|
+
@lines[opening.span] = @syntax.opening(snippet, opening.copyrights)
|
|
73
|
+
@notices.concat(renewal.notices)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "copyright"
|
|
9
|
+
require_relative "renewal"
|
|
10
|
+
|
|
11
|
+
class SpdxHeader < Data.define(:copyrights, :license) # :nodoc:
|
|
12
|
+
def self.of(author, contributions, license)
|
|
13
|
+
copyrights = if contributions.empty?
|
|
14
|
+
[Copyright.yours(author)]
|
|
15
|
+
else
|
|
16
|
+
contributions.map { |contribution| Copyright.of(contribution) }.sort_by(&:holder)
|
|
17
|
+
end
|
|
18
|
+
new(copyrights:, license:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def renewal(contributions, license)
|
|
22
|
+
renewals = copyrights.map { |copyright| copyright.renewal(contributions) }
|
|
23
|
+
renewed_copyrights = renewals.map(&:value)
|
|
24
|
+
notices = renewals.flat_map(&:notices)
|
|
25
|
+
contributions.each do |contribution|
|
|
26
|
+
next if renewed_copyrights.any? { |copyright| copyright.held_by?(contribution.author) }
|
|
27
|
+
|
|
28
|
+
notices << "Adding #{contribution.author.name}'s copyright"
|
|
29
|
+
renewed_copyrights << Copyright.of(contribution)
|
|
30
|
+
end
|
|
31
|
+
notices << "Fixing license: #{self.license} -> #{license}" if self.license != license
|
|
32
|
+
|
|
33
|
+
Renewal.new(value: with(copyrights: renewed_copyrights, license:), notices:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def commented(prefix)
|
|
37
|
+
# REUSE-IgnoreStart
|
|
38
|
+
copyrights.map { |copyright| "#{prefix} SPDX-FileCopyrightText: #{copyright}\n" }.join +
|
|
39
|
+
"#{prefix} SPDX-License-Identifier: #{license}\n"
|
|
40
|
+
# REUSE-IgnoreEnd
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
class UnknownFile < Data.define(:path) # :nodoc:
|
|
9
|
+
def annotate
|
|
10
|
+
["Skipping #{path} (unknown extension: .#{File.extname(path).delete_prefix('.')})"]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "licensed_file"
|
|
9
|
+
require_relative "licensing"
|
|
10
|
+
require_relative "markdown_header_syntax"
|
|
11
|
+
require_relative "rbs_header_syntax"
|
|
12
|
+
require_relative "reuse_annotated_file"
|
|
13
|
+
require_relative "ruby_header_syntax"
|
|
14
|
+
require_relative "rust_header_syntax"
|
|
15
|
+
require_relative "unknown_file"
|
|
16
|
+
|
|
17
|
+
# The files the REUSE Tool finds without headers. The license tasks know each
|
|
18
|
+
# language's header and who to credit, so the tool annotates only the rest.
|
|
19
|
+
class UnlicensedFiles # :nodoc:
|
|
20
|
+
CODE_EXTENSIONS = %w[rb rs rake gemspec rbs toml yml yaml json lock].freeze
|
|
21
|
+
DOC_EXTENSIONS = %w[md txt].freeze
|
|
22
|
+
DOC_LICENSE = "CC-BY-SA-4.0"
|
|
23
|
+
|
|
24
|
+
def initialize(tool, repository, overrides, licensing)
|
|
25
|
+
@tool = tool
|
|
26
|
+
@repository = repository
|
|
27
|
+
@overrides = overrides
|
|
28
|
+
@licensing = licensing
|
|
29
|
+
@syntaxes = [
|
|
30
|
+
MarkdownHeaderSyntax.new,
|
|
31
|
+
RubyHeaderSyntax.new(licensing),
|
|
32
|
+
RbsHeaderSyntax.new(licensing),
|
|
33
|
+
RustHeaderSyntax.new(licensing),
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Licenses what the tool found and says what it did.
|
|
38
|
+
def license
|
|
39
|
+
paths = @tool.unlicensed_paths
|
|
40
|
+
return ["All files have REUSE headers!"] if paths.empty?
|
|
41
|
+
|
|
42
|
+
paths.flat_map { |path| file_at(path).annotate.map { |message| " #{message}" } }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def file_at(path)
|
|
46
|
+
syntax = @syntaxes.find { |candidate| candidate.covers?(path) }
|
|
47
|
+
return LicensedFile.new(path:, syntax:, author:) if syntax
|
|
48
|
+
|
|
49
|
+
license = license_for(path)
|
|
50
|
+
return UnknownFile.new(path:) unless license
|
|
51
|
+
|
|
52
|
+
ReuseAnnotatedFile.new(path:, license:, copyright:, tool: @tool)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private def license_for(path)
|
|
56
|
+
case File.extname(path).delete_prefix(".")
|
|
57
|
+
when *CODE_EXTENSIONS then @overrides.fetch("REUSE_CODE_LICENSE") { @licensing.license_for(path) }
|
|
58
|
+
when *DOC_EXTENSIONS then @overrides.fetch("REUSE_DOC_LICENSE", DOC_LICENSE)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def copyright
|
|
63
|
+
@overrides.fetch("REUSE_COPYRIGHT") { author.to_s }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def author
|
|
67
|
+
@author ||= @repository.author
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# A Snippet that already has its wrapper, opened by its SnippetOpening.
|
|
9
|
+
class WrappedSnippet < Data.define(:snippet, :opening) # :nodoc:
|
|
10
|
+
def first
|
|
11
|
+
snippet.first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def covers?(other)
|
|
15
|
+
other.first.between?(snippet.first, snippet.last)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def blame_range
|
|
19
|
+
snippet.blame_range
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bare?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def revise(revision)
|
|
27
|
+
renewal = opening.renewal(revision.contributions(self), revision.license)
|
|
28
|
+
revision.reopen(snippet, renewal) if renewal.changed?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rake/tasklib"
|
|
9
|
+
|
|
10
|
+
require_relative "license/changed_files"
|
|
11
|
+
require_relative "license/licensed_file"
|
|
12
|
+
require_relative "license/markdown_header_syntax"
|
|
13
|
+
require_relative "license/markdown_snippet_syntax"
|
|
14
|
+
require_relative "license/rbs_header_syntax"
|
|
15
|
+
require_relative "license/rdoc_snippet_syntax"
|
|
16
|
+
require_relative "license/ruby_header_syntax"
|
|
17
|
+
require_relative "license/rust_header_syntax"
|
|
18
|
+
require_relative "license/snippet_file"
|
|
19
|
+
|
|
20
|
+
# The tasks that write license headers and snippet wrappers.
|
|
21
|
+
class LicenseTasks < Rake::TaskLib # :nodoc:
|
|
22
|
+
def initialize(licensing)
|
|
23
|
+
super()
|
|
24
|
+
@licensing = licensing
|
|
25
|
+
define
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private def define
|
|
29
|
+
namespace :license do
|
|
30
|
+
namespace :headers do
|
|
31
|
+
{
|
|
32
|
+
md: ["Ensure markdown files have correct CC-BY-SA-4.0 headers", MarkdownHeaderSyntax.new],
|
|
33
|
+
rb: ["Ensure Ruby files have correct SPDX headers", RubyHeaderSyntax.new(@licensing)],
|
|
34
|
+
rbs: ["Ensure RBS files have correct SPDX headers", RbsHeaderSyntax.new(@licensing)],
|
|
35
|
+
rs: ["Ensure Rust files have correct SPDX headers", RustHeaderSyntax.new(@licensing)],
|
|
36
|
+
}.each do |name, (description, syntax)|
|
|
37
|
+
desc description
|
|
38
|
+
task name, [:files] do |_t, args|
|
|
39
|
+
LicensedFile.tracked(args[:files].to_s.split, syntax).flat_map(&:annotate).each { |message| puts message }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "Ensure all files have correct license headers"
|
|
44
|
+
task all: %w[md rb rbs rs]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :snippets do
|
|
48
|
+
{
|
|
49
|
+
md: ["Add MIT-0 SPDX snippet headers to markdown fenced code blocks", MarkdownSnippetSyntax, ""],
|
|
50
|
+
rdoc: ["Add MIT-0 SPDX snippet headers to RDoc code examples in Ruby files", RdocSnippetSyntax, "lib/"],
|
|
51
|
+
}.each do |name, (description, syntax, default)|
|
|
52
|
+
desc description
|
|
53
|
+
task name, [:files] do |_t, args|
|
|
54
|
+
SnippetFile.tracked((args[:files] || default).split, syntax.new).flat_map(&:annotate).each { |message| puts message }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "Add MIT-0 SPDX snippet headers to all code examples"
|
|
59
|
+
task all: %w[md rdoc]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Run all license tasks (headers + snippets)"
|
|
63
|
+
task all: ["headers:all", "snippets:all"]
|
|
64
|
+
|
|
65
|
+
desc "Run license tasks on changed files only (staged + unstaged)"
|
|
66
|
+
task :new do
|
|
67
|
+
ChangedFiles.new(Repository.new, @licensing).license.each { |message| puts message }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# The gems Bundler locked for this project.
|
|
9
|
+
#
|
|
10
|
+
# CI installs the lockfile on every supported Ruby, but Bundler only checks
|
|
11
|
+
# gems against the Ruby that resolves it. A lockfile resolved on the newest
|
|
12
|
+
# Ruby can hold gems the oldest one can't install.
|
|
13
|
+
class GemLockfile < Data.define(:specs) # :nodoc:
|
|
14
|
+
def gems_too_new_for(ruby)
|
|
15
|
+
specs.reject { |spec| spec.required_ruby_version.satisfied_by?(ruby) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify(ruby)
|
|
19
|
+
too_new = gems_too_new_for(ruby)
|
|
20
|
+
return if too_new.empty?
|
|
21
|
+
|
|
22
|
+
abort <<~MESSAGE
|
|
23
|
+
These locked gems need a newer Ruby than #{ruby}, the oldest the gemspec supports:
|
|
24
|
+
#{too_new.map { |spec| " #{spec.full_name} (Ruby #{spec.required_ruby_version})" }.join("\n")}
|
|
25
|
+
Resolve them with that Ruby instead:
|
|
26
|
+
mise x ruby@#{ruby} -- bundle lock --update #{too_new.map(&:name).join(' ')}
|
|
27
|
+
MESSAGE
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# The Ruby versions a gemspec supports.
|
|
9
|
+
class SupportedRubies < Data.define(:requirement) # :nodoc:
|
|
10
|
+
def self.of(spec)
|
|
11
|
+
new(requirement: spec.required_ruby_version)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def oldest
|
|
15
|
+
_, version = requirement.requirements.find { |operator, _| operator == ">=" }
|
|
16
|
+
version
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
require "rubycritic/rake_task"
|
|
10
|
+
require "inch/rake"
|
|
11
|
+
require "rake/tasklib"
|
|
12
|
+
|
|
13
|
+
# The tasks that lint a gem.
|
|
14
|
+
class LintTasks < Rake::TaskLib # :nodoc:
|
|
15
|
+
def initialize(gem)
|
|
16
|
+
super()
|
|
17
|
+
@gem = gem
|
|
18
|
+
define
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private def define
|
|
22
|
+
RuboCop::RakeTask.new
|
|
23
|
+
|
|
24
|
+
# Run rubycritic in a shell to prevent it from exiting the rake process
|
|
25
|
+
task :rubycritic do
|
|
26
|
+
paths = []
|
|
27
|
+
paths << "exe" if Dir.exist?("exe")
|
|
28
|
+
paths << "lib" if Dir.exist?("lib")
|
|
29
|
+
paths << "sig" if Dir.exist?("sig")
|
|
30
|
+
sh "bundle exec rubycritic --no-browser #{paths.join(' ')}" unless paths.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Inch::Rake::Suggest.new("doc:suggest", "exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs") do |suggest|
|
|
34
|
+
suggest.args << ""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
namespace :lint do
|
|
38
|
+
task :safe_rdoc_coverage do
|
|
39
|
+
if Rake::Task.task_defined?("rdoc:coverage")
|
|
40
|
+
sh "bundle exec rake rdoc:coverage"
|
|
41
|
+
else
|
|
42
|
+
puts "rdoc:coverage task not defined, skipping"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Check that every locked gem installs on the oldest supported Ruby"
|
|
47
|
+
task :lockfile_ruby do
|
|
48
|
+
require_relative "lint/gem_lockfile"
|
|
49
|
+
require_relative "lint/supported_rubies"
|
|
50
|
+
|
|
51
|
+
GemLockfile.new(specs: Bundler.load.specs.to_a).verify(SupportedRubies.of(@gem.spec).oldest)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Build dynamic task lists based on what's available
|
|
55
|
+
docs_tasks = %w[rubycritic]
|
|
56
|
+
docs_tasks.unshift("autodoc") if Rake::Task.task_defined?("autodoc")
|
|
57
|
+
docs_tasks << "safe_rdoc_coverage" if Rake::Task.task_defined?("rdoc:coverage")
|
|
58
|
+
|
|
59
|
+
code_tasks = %w[rubocop rubycritic lockfile_ruby]
|
|
60
|
+
code_tasks += %w[cargo:fmt cargo:clippy cargo:test] if Dir.exist?("ext")
|
|
61
|
+
|
|
62
|
+
task docs: docs_tasks
|
|
63
|
+
task code: code_tasks
|
|
64
|
+
task all: %w[docs code]
|
|
65
|
+
|
|
66
|
+
namespace :fix do
|
|
67
|
+
desc "Auto-fix RuboCop offenses (most aggressive)"
|
|
68
|
+
task :rubocop do
|
|
69
|
+
sh "bundle exec rubocop --autocorrect-all"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if Dir.exist?("ext")
|
|
73
|
+
desc "Auto-fix Clippy warnings (most aggressive: --fix --allow-dirty --allow-staged)"
|
|
74
|
+
task :clippy do
|
|
75
|
+
ext_dirs = Dir.glob("ext/*").select { |d| File.directory?(d) && File.exist?("#{d}/Cargo.toml") }
|
|
76
|
+
ext_dirs.each do |dir|
|
|
77
|
+
sh "cd #{dir} && cargo clippy --fix --allow-dirty --allow-staged"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Build dynamic fix:all task
|
|
83
|
+
fix_all_tasks = %w[lint:fix:rubocop]
|
|
84
|
+
fix_all_tasks.insert(1, "lint:fix:clippy") if Dir.exist?("ext")
|
|
85
|
+
|
|
86
|
+
desc "Run all auto-fix tasks"
|
|
87
|
+
task all: fix_all_tasks
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc "Run all lint auto-fix tasks"
|
|
92
|
+
task("lint:fix") { Rake::Task["lint:fix:all"].invoke }
|
|
93
|
+
|
|
94
|
+
# Aliases for convenience
|
|
95
|
+
task "rubocop:autocorrect_all" => "lint:fix:rubocop"
|
|
96
|
+
|
|
97
|
+
desc "Run all lint tasks"
|
|
98
|
+
task(:lint) { Rake::Task["lint:all"].invoke }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -17,21 +17,20 @@ require_relative "versioned_binary"
|
|
|
17
17
|
# A PlatformGem collects those binaries and packages them into a single
|
|
18
18
|
# installable gem that serves all supported Ruby versions.
|
|
19
19
|
class PlatformGem
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@binaries = VersionedBinary.scan(lib_dir, RatatuiRuby::Devtools.gem_name)
|
|
20
|
+
# Scans the gem's library for compiled binaries.
|
|
21
|
+
#
|
|
22
|
+
# [gem] The RubyGem to package.
|
|
23
|
+
def initialize(gem)
|
|
24
|
+
@spec = gem.spec
|
|
25
|
+
@lib_dir = "lib/#{gem.name.tr('-', '_')}"
|
|
26
|
+
@binaries = VersionedBinary.scan(@lib_dir, gem.name)
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
# Packages the platform gem into +pkg/+.
|
|
31
30
|
def build
|
|
32
31
|
abort "No versioned binaries found in #{@lib_dir}/*/" if @binaries.empty?
|
|
33
32
|
|
|
34
|
-
@spec.platform =
|
|
33
|
+
@spec.platform = platform
|
|
35
34
|
@spec.extensions.clear
|
|
36
35
|
@spec.files += @binaries.map(&:path)
|
|
37
36
|
|
|
@@ -56,8 +55,18 @@ class PlatformGem
|
|
|
56
55
|
FileUtils.mv(gem_file, "pkg")
|
|
57
56
|
puts "Built pkg/#{File.basename(gem_file)}"
|
|
58
57
|
end
|
|
59
|
-
end
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
# The platform of the packaging Ruby, without a darwin version.
|
|
60
|
+
#
|
|
61
|
+
# RubyGems installs an <tt>arm64-darwin-23</tt> gem only on a Ruby that
|
|
62
|
+
# reports exactly darwin 23. That number names the CI image's macOS, not
|
|
63
|
+
# anything the binary needs. The binary targets macOS 11, the first
|
|
64
|
+
# release on Apple Silicon, so <tt>arm64-darwin</tt> fits every Mac
|
|
65
|
+
# that can run it.
|
|
66
|
+
private def platform
|
|
67
|
+
local = Gem::Platform.local
|
|
68
|
+
return local unless local.os == "darwin"
|
|
69
|
+
|
|
70
|
+
Gem::Platform.new([local.cpu, local.os, nil])
|
|
71
|
+
end
|
|
63
72
|
end
|
|
@@ -11,7 +11,7 @@ require_relative "../bump/sem_ver"
|
|
|
11
11
|
class VersionedBinary < Data.define(:path)
|
|
12
12
|
##
|
|
13
13
|
# Returns sorted VersionedBinary instances found under +lib_dir+.
|
|
14
|
-
def self.scan(lib_dir, gem_name
|
|
14
|
+
def self.scan(lib_dir, gem_name)
|
|
15
15
|
Dir.glob("#{lib_dir}/*/#{gem_name.tr('-', '_')}.*")
|
|
16
16
|
.reject { |p| p.end_with?(".rb") }
|
|
17
17
|
.map { |p| new(path: p) }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rake/tasklib"
|
|
9
|
+
|
|
10
|
+
require_relative "bump/repository"
|
|
11
|
+
require_relative "release/platform_gem"
|
|
12
|
+
|
|
13
|
+
# The tasks that release a gem and its native gems.
|
|
14
|
+
class ReleaseTasks < Rake::TaskLib # :nodoc:
|
|
15
|
+
def initialize(gem, repository: Repository.new)
|
|
16
|
+
super()
|
|
17
|
+
@gem = gem
|
|
18
|
+
@repository = repository
|
|
19
|
+
define
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def define
|
|
23
|
+
namespace :release do
|
|
24
|
+
desc "Package the precompiled binaries in lib/ into a platform gem"
|
|
25
|
+
task :platform_gem do
|
|
26
|
+
PlatformGem.new(@gem).build
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Update stable branch to match release and set as default"
|
|
30
|
+
task :update_stable do
|
|
31
|
+
version = @gem.release_version
|
|
32
|
+
|
|
33
|
+
# A release that failed leaves the version file ahead of the tags, and
|
|
34
|
+
# stable would then point at the wrong version.
|
|
35
|
+
latest_tag = @repository.latest_tag
|
|
36
|
+
if latest_tag != version.to_tag
|
|
37
|
+
abort "Fatal: Version mismatch! '#{@gem.version_file}' says #{version.to_tag}, " \
|
|
38
|
+
"but the latest git tag is #{latest_tag}."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
stable = @gem.stable_version(@repository)
|
|
42
|
+
if stable && version.to_gem_version <= stable
|
|
43
|
+
puts "Skipping stable update: #{version} is not newer than current stable #{stable}"
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "Updating stable branch to point to #{version.to_tag}..."
|
|
48
|
+
@repository.move_stable(@repository.released_commit(version.to_tag))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if Rake::Task.task_defined?("release")
|
|
53
|
+
release = Rake::Task["release"]
|
|
54
|
+
|
|
55
|
+
# Bundler tags and publishes in the release task's prerequisites, so a
|
|
56
|
+
# version its tag can't name has to stop the release before they run.
|
|
57
|
+
task "release:check_version" do
|
|
58
|
+
@gem.release_version
|
|
59
|
+
end
|
|
60
|
+
release.prerequisites.unshift("release:check_version")
|
|
61
|
+
|
|
62
|
+
release.enhance do
|
|
63
|
+
version = @gem.release_version
|
|
64
|
+
|
|
65
|
+
# Bundler tags the release v plus the version as RubyGems writes it, so
|
|
66
|
+
# a prerelease gets a tag like v1.0.0.pre.beta.1 that isn't SemVer.
|
|
67
|
+
normalized_tag = "v#{version.to_gem_version}"
|
|
68
|
+
if normalized_tag != version.to_tag
|
|
69
|
+
puts "Replacing normalized tag #{normalized_tag} with semver tag #{version.to_tag}..."
|
|
70
|
+
@repository.delete_tag(normalized_tag)
|
|
71
|
+
@repository.tag_head(version.to_tag)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@gem.crate.release_native_gems(version: version.to_s, sha: @repository.released_commit(version.to_tag))
|
|
75
|
+
|
|
76
|
+
Rake::Task["release:update_stable"].invoke
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rake/tasklib"
|
|
9
|
+
|
|
10
|
+
require_relative "bump/repository"
|
|
11
|
+
require_relative "license/reuse_tool"
|
|
12
|
+
require_relative "license/unlicensed_files"
|
|
13
|
+
|
|
14
|
+
# The tasks that check and fix REUSE compliance.
|
|
15
|
+
class ReuseTasks < Rake::TaskLib # :nodoc:
|
|
16
|
+
def initialize(licensing)
|
|
17
|
+
super()
|
|
18
|
+
@licensing = licensing
|
|
19
|
+
define
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def define
|
|
23
|
+
namespace :reuse do
|
|
24
|
+
desc "Run the REUSE Tool to confirm REUSE compliance"
|
|
25
|
+
task :lint do
|
|
26
|
+
sh "reuse lint"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Add SPDX headers to files missing them (per AGENTS.md standards)"
|
|
30
|
+
task :fix do
|
|
31
|
+
puts "Checking for files missing REUSE headers..."
|
|
32
|
+
overrides = ENV.slice("REUSE_COPYRIGHT", "REUSE_CODE_LICENSE", "REUSE_DOC_LICENSE")
|
|
33
|
+
UnlicensedFiles.new(ReuseTool.new, Repository.new, overrides, @licensing).license.each { |message| puts message }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
task(:reuse) { Rake::Task["reuse:lint"].invoke }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -5,14 +5,23 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
+
require "rake/tasklib"
|
|
9
|
+
require "minitest/test_task"
|
|
10
|
+
|
|
8
11
|
# Minitest task for pure Ruby gems.
|
|
9
12
|
#
|
|
10
13
|
# Consumer gems need test running. This provides a simple Minitest::TestTask
|
|
11
14
|
# that discovers tests in the standard locations. Gems with Rust extensions
|
|
12
15
|
# should define their own test task that includes cargo:test.
|
|
16
|
+
class TestTasks < Rake::TaskLib # :nodoc:
|
|
17
|
+
def initialize
|
|
18
|
+
super
|
|
19
|
+
define
|
|
20
|
+
end
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
private def define
|
|
23
|
+
Minitest::TestTask.create(:test) do |t|
|
|
24
|
+
t.test_globs = ["test/**/*_test.rb", "test/**/test_*.rb"]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
18
27
|
end
|