udb-gen 0.1.1

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/bin/udb-gen +57 -0
  4. data/lib/udb-gen/adoc_helpers.rb +83 -0
  5. data/lib/udb-gen/common_opts.rb +51 -0
  6. data/lib/udb-gen/defines.rb +14 -0
  7. data/lib/udb-gen/generators/ext_doc/generator.rb +228 -0
  8. data/lib/udb-gen/generators/ext_doc/helpers.rb +63 -0
  9. data/lib/udb-gen/generators/isa_explorer/generator.rb +247 -0
  10. data/lib/udb-gen/generators/isa_explorer/js_xlsx_writer.rb +140 -0
  11. data/lib/udb-gen/generators/isa_explorer/table_builder.rb +244 -0
  12. data/lib/udb-gen/generators/manual/generator.rb +124 -0
  13. data/lib/udb-gen/generators/manual/tasks.rake +472 -0
  14. data/lib/udb-gen/subcommand.rb +33 -0
  15. data/lib/udb-gen/template_helpers.rb +153 -0
  16. data/lib/udb-gen/version.rb +9 -0
  17. data/lib/udb-gen.rb +9 -0
  18. data/templates/common/csr.adoc.erb +133 -0
  19. data/templates/common/inst.adoc.erb +130 -0
  20. data/templates/common/mmr.adoc.erb +71 -0
  21. data/templates/ext_doc/_csr_list.adoc.erb +49 -0
  22. data/templates/ext_doc/_header.adoc.erb +57 -0
  23. data/templates/ext_doc/_idl_functions.adoc.erb +47 -0
  24. data/templates/ext_doc/_instruction_list.adoc.erb +85 -0
  25. data/templates/ext_doc/_preamble.adoc.erb +89 -0
  26. data/templates/ext_doc/ext_pdf.adoc.erb +144 -0
  27. data/templates/isa_explorer/csr-browser.html.erb +21 -0
  28. data/templates/isa_explorer/ext-browser.html.erb +21 -0
  29. data/templates/isa_explorer/inst-browser.html.erb +21 -0
  30. data/templates/isa_explorer/load_table.js +39 -0
  31. data/templates/manual/csr.adoc.erb +172 -0
  32. data/templates/manual/ext.adoc.erb +85 -0
  33. data/templates/manual/func.adoc.erb +45 -0
  34. data/templates/manual/highlight.js +3580 -0
  35. data/templates/manual/index.adoc.erb +16 -0
  36. data/templates/manual/instruction.adoc.erb +180 -0
  37. data/templates/manual/isa_nav.adoc.erb +29 -0
  38. data/templates/manual/isa_version_index.adoc.erb +12 -0
  39. data/templates/manual/param_list.adoc.erb +22 -0
  40. data/templates/manual/playbook.yml.erb +130 -0
  41. metadata +270 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92f11f140e0d89cf83792ae8c4b426710967352ba39d226e979b65f1f06fed1c
4
+ data.tar.gz: 55d515c69db630f9d1ba0c15fd8271f808a663469ebb998c50d46e15b40402c3
5
+ SHA512:
6
+ metadata.gz: 247ac625e6bf5362c903cd39255ec1627cb3caffb535954cf9d69bf0e6afb7171dceaa15a49c2078bbe0d5c59c6a612e22b77db266ba914eb0d3afda702c20be
7
+ data.tar.gz: e597dc22ab56635f8ea312a69fee7c9766068ec1a0c6bdeee0c679d4258832b0777af499445a3062aa86b7fc59248a6a3a5a3a1a91f3bd327601ec2196643484
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ SPDX-License-Identifier: BSD-3-Clause-Clear
2
+
3
+ Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are permitted
6
+ (subject to the limitations in the disclaimer below) provided that the following conditions are
7
+ met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
10
+ and the following disclaimer.
11
+ * Redistributions in binary form must reproduce the above copyright notice, this list of
12
+ conditions and the following disclaimer in the documentation and/or other materials provided
13
+ with the distribution.
14
+ * Neither the name of Qualcomm Innovation Center, Inc. nor the names of its contributors may be used
15
+ to endorse or promote products derived from this software without specific prior written
16
+ permission.
17
+
18
+ NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS
19
+ SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
20
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
22
+ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
24
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
26
+ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/bin/udb-gen ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
3
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
4
+
5
+ # typed: false
6
+ # frozen_string_literal: true
7
+
8
+ require "sorbet-runtime"
9
+ require "tty-table"
10
+
11
+ # Auto-load all generators
12
+ Dir[File.join(__dir__, "../lib/udb-gen/generators/*/generator.rb")].sort.each do |f|
13
+ require f
14
+ end
15
+
16
+ include TTY::Exit
17
+
18
+ def subcommands
19
+ @subcommands ||=
20
+ UdbGen::Subcommand.subclasses.map do |cmd_klass|
21
+ if cmd_klass.name == "UdbGen::SubcommandWithCommonOptions"
22
+ cmd_klass.subclasses.map do |subcmd_klass|
23
+ subcmd_klass.new
24
+ end
25
+ else
26
+ cmd_klass.new
27
+ end
28
+ end.flatten.sort { |a, b| a.name <=> b.name }
29
+ end
30
+
31
+ def subcommand_table
32
+ t = TTY::Table.new
33
+ subcommands.each do |cmd|
34
+ t << [cmd.name, cmd.desc]
35
+ end
36
+ t
37
+ end
38
+
39
+ def usage
40
+ <<~USAGE
41
+ #{$PROGRAM_NAME} SUB-COMMAND [OPTIONS]
42
+
43
+ Subcommands:
44
+ #{subcommand_table.render(:basic, indent: 4, multiline: true, padding: [0, 2, 0, 0])}
45
+
46
+ Run `#{$PROGRAM_NAME}` SUB-COMMAND --help` for details
47
+ USAGE
48
+ end
49
+
50
+ if ARGV.empty? || subcommands.find { |cmd| cmd.name == ARGV[0] }.nil?
51
+ puts usage
52
+
53
+ exit_with(:usage_error)
54
+ end
55
+
56
+ subcmd = subcommands.find { |cmd| cmd.name == ARGV[0] }
57
+ subcmd.run(ARGV[1..])
@@ -0,0 +1,83 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "sorbet-runtime"
8
+
9
+ module UdbGen
10
+ module AdocHelpers
11
+ extend T::Sig
12
+ include Kernel
13
+
14
+ # Returns the :revmark: attribute value for the given ExtensionVersion.
15
+ sig { params(version: Udb::ExtensionVersion).returns(String) }
16
+ def state_revmark(version)
17
+ case version.state
18
+ when "ratified"
19
+ <<~STATE
20
+ This document is in the http://riscv.org/spec-state[Ratified state] + \\
21
+ + \\
22
+ No changes are allowed. + \\
23
+ Any desired or needed changes can be the subject of a follow-on new extension. + \\
24
+ Ratified extensions are never revised. + \\
25
+ STATE
26
+ when "frozen"
27
+ <<~STATE
28
+ This document is in the http://riscv.org/spec-state[Frozen state]. + \\
29
+ + \\
30
+ Change is extremely unlikely. + \\
31
+ A high threshold will be used, and a change will only occur because of some truly + \\
32
+ critical issue being identified during the public review cycle. + \\
33
+ Any other desired or needed changes can be the subject of a follow-on new extension. + \\
34
+ STATE
35
+ when "development"
36
+ <<~STATE
37
+ This document is in the http://riscv.org/spec-state[Development state]. + \\
38
+ + \\
39
+ Change should be expected + \\
40
+ STATE
41
+ else
42
+ raise "Unknown state: #{version.state}"
43
+ end
44
+ end
45
+
46
+ # Returns the preamble [WARNING] admonition block for the given ExtensionVersion.
47
+ sig { params(version: Udb::ExtensionVersion).returns(String) }
48
+ def state_preamble_adoc(version)
49
+ case version.state
50
+ when "ratified"
51
+ <<~ADOC
52
+ [WARNING]
53
+ .This document is in the link:http://riscv.org/spec-state[Ratified state]
54
+ ====
55
+ No changes are allowed. Any desired or needed changes can be the subject of a
56
+ follow-on new extension. Ratified extensions are never revised
57
+ ====
58
+ ADOC
59
+ when "frozen"
60
+ <<~ADOC
61
+ [WARNING]
62
+ This document is in the http://riscv.org/spec-state[Frozen state].
63
+ ====
64
+ Change is extremely unlikely.
65
+ A high threshold will be used, and a change will only occur because of some truly
66
+ critical issue being identified during the public review cycle.
67
+ Any other desired or needed changes can be the subject of a follow-on new extension.
68
+ ====
69
+ ADOC
70
+ when "development"
71
+ <<~ADOC
72
+ [WARNING]
73
+ This document is in the http://riscv.org/spec-state[Development state].
74
+ ====
75
+ Change should be expected
76
+ ====
77
+ ADOC
78
+ else
79
+ raise "Unknown state: #{version.state}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,51 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "sorbet-runtime"
8
+
9
+ require_relative "subcommand"
10
+
11
+ module UdbGen
12
+ class SubcommandWithCommonOptions < Subcommand
13
+ extend T::Sig
14
+ include TTY::Option
15
+
16
+ sig { params(name: String, desc: String).void }
17
+ def initialize(name:, desc:)
18
+ super(name:, desc:)
19
+ end
20
+
21
+ option :cfg do
22
+ T.bind(self, TTY::Option::Parameter::Option)
23
+ short "-c"
24
+ long "--cfg=cfg_name"
25
+ default "_"
26
+ end
27
+
28
+ flag :help do
29
+ T.bind(self, TTY::Option::Parameter::Option)
30
+ short "-h"
31
+ long "--help"
32
+ desc "Print usage"
33
+ end
34
+
35
+ sig { returns(Udb::Resolver) }
36
+ def resolver
37
+ @resolver ||= Udb::Resolver.new
38
+ end
39
+
40
+ sig { returns(Udb::ConfiguredArchitecture) }
41
+ def cfg_arch
42
+ @cfg_arch ||=
43
+ resolver.cfg_arch_for(params[:cfg])
44
+ end
45
+
46
+ sig { override.params(argv: T::Array[String]).returns(T.noreturn) }
47
+ def run(argv)
48
+ raise "must override #run in #{self.class.name}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "sorbet-runtime"
8
+
9
+ module UdbGen
10
+ extend T::Sig
11
+
12
+ sig { returns(Pathname) }
13
+ def self.root = (Pathname.new(__dir__) / ".." / "..").realpath
14
+ end
@@ -0,0 +1,228 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "tty-exit"
8
+
9
+ require_relative "../../common_opts"
10
+ require_relative "../../defines"
11
+ require_relative "../../template_helpers"
12
+ require_relative "helpers"
13
+
14
+ require "udb/obj/extension"
15
+
16
+ module UdbGen
17
+ class GenExtPdfOptions < SubcommandWithCommonOptions
18
+ include TTY::Exit
19
+ include TemplateHelpers
20
+ include ExtDocHelpers
21
+ include AdocHelpers
22
+
23
+ NAME = "ext-doc"
24
+
25
+ sig { void }
26
+ def initialize
27
+ super(name: NAME, desc: "Create documentation for an extension")
28
+ end
29
+
30
+ usage \
31
+ command: NAME,
32
+ desc: "Generate documentation for an extension defined in UDB",
33
+ example: <<~EXAMPLE
34
+ Generate documentation for the Zba extension
35
+ $ #{File.basename($PROGRAM_NAME)} #{NAME} -o Zba.pdf Zba
36
+
37
+ Generate documentation for the B extension and all it's subextensions
38
+ $ #{File.basename($PROGRAM_NAME)} #{NAME} -o B.pdf B Zba Zbb Zbss
39
+
40
+ Generate documentation for the A extension version 2.1.0
41
+ $ #{File.basename($PROGRAM_NAME)} #{NAME} -o A.pdf A@2.1.0
42
+
43
+ Generate documentation for the A extension latest version
44
+ $ #{File.basename($PROGRAM_NAME)} #{NAME} -o A.pdf A@latest
45
+
46
+ Generate documentation for the A extension, all versions >= 2.1.0
47
+ $ #{File.basename($PROGRAM_NAME)} #{NAME} -o A.pdf "A@>=2.1.0"
48
+ EXAMPLE
49
+
50
+ argument :extension do
51
+ T.bind(self, TTY::Option::Parameter::Argument)
52
+ arity one_or_more
53
+ desc "Extension(s) to generate documentation for."
54
+ validate ->(e) { e =~ /^(([A-WY])|([SXZ][a-z0-9]+))(@([0-9]+)(?:\.([0-9]+)(?:\.([0-9]+)(?:-(pre))?)?)?)?$/ }
55
+ convert :list
56
+ end
57
+
58
+ flag :include_implies do
59
+ T.bind(self, TTY::Option::Parameter::Option)
60
+ short "-i"
61
+ long "--implied_insts"
62
+ desc "Include a list of implied instructions when describing extension(s)"
63
+ end
64
+
65
+ flag :exclude_csr_field_descriptions do
66
+ T.bind(self, TTY::Option::Parameter::Option)
67
+ long "--no-csr-field-desc"
68
+ desc "Do not generate a long descrption of each CSR field (just a summary table)"
69
+ end
70
+
71
+ option :pseudo do
72
+ T.bind(self, TTY::Option::Parameter::Option)
73
+ short "-p"
74
+ long "--p=type"
75
+ desc "Which pdeudocode(s) to include in the documentation"
76
+ permit ["sail", "idl", "both"]
77
+ default "idl"
78
+ end
79
+
80
+ option :format do
81
+ T.bind(self, TTY::Option::Parameter::Option)
82
+ short "-f"
83
+ long "--f=format"
84
+ desc "Output format"
85
+ permit ["pdf"]
86
+ default "pdf"
87
+ end
88
+
89
+ option :output_dir do
90
+ T.bind(self, TTY::Option::Parameter::Option)
91
+ required
92
+ short "-o"
93
+ long "--out=directory"
94
+ desc "Output directory"
95
+ convert :path
96
+ end
97
+
98
+ option :output_basename do
99
+ T.bind(self, TTY::Option::Parameter::Option)
100
+ desc "Basename of the output files. Default is the name of the first listed extension"
101
+ short "-b"
102
+ long "--output-basename=basename"
103
+ end
104
+
105
+ option :theme do
106
+ T.bind(self, TTY::Option::Parameter::Option)
107
+ desc "Theme file for asciidoctor-pdf"
108
+ long "--theme=path"
109
+ convert :path
110
+ default UdbGen.root / "riscv-docs-resources" / "themes" / "riscv-pdf.yml"
111
+ end
112
+
113
+ option :fonts do
114
+ T.bind(self, TTY::Option::Parameter::Option)
115
+ desc "Fonts directory"
116
+ long "--fonts=path"
117
+ convert :path
118
+ default UdbGen.root / "riscv-docs-resources" / "fonts"
119
+ end
120
+
121
+ option :images do
122
+ T.bind(self, TTY::Option::Parameter::Option)
123
+ desc "Images directory"
124
+ long "--images=path"
125
+ convert :path
126
+ default UdbGen.root / "riscv-docs-resources" / "images"
127
+ end
128
+
129
+ option :debug do
130
+ T.bind(self, TTY::Option::Parameter::Option)
131
+ desc "Set debug level"
132
+ long "--debug=level"
133
+ short "-d"
134
+ default "info"
135
+ permit ["debug", "info", "warn", "error", "fatal"]
136
+ end
137
+
138
+ def basename
139
+ params[:output_basename].nil? ? params[:extension][0] : params[:output_basename]
140
+ end
141
+
142
+ def gen_adoc
143
+ ext_reqs = T.let([], T::Array[Udb::ExtensionRequirement])
144
+ Udb.log_level = Udb::LogLevel.deserialize(params[:debug])
145
+ params[:extension].each do |ext_req_str|
146
+ ext_name, req = ext_req_str.split("@")
147
+ ext = cfg_arch.extension(ext_name)
148
+ exit_with(:data_err, "No extension named '#{ext_name}'\n") if ext.nil?
149
+ req =
150
+ case req
151
+ when nil
152
+ ">=0"
153
+ when "latest"
154
+ "=#{ext.versions.max}"
155
+ else
156
+ "=#{req}"
157
+ end
158
+ ext_reqs << cfg_arch.extension_requirement(ext_name, req)
159
+ end
160
+
161
+ primary_ext = ext_reqs.fetch(0).extension
162
+ max_version = ext_reqs.fetch(0).satisfying_versions.max
163
+
164
+ template_path = Pathname.new(Gem.loaded_specs["udb-gen"].full_gem_path) / "templates" / "ext_doc" / "ext_pdf.adoc.erb"
165
+ gen_filename = params[:output_dir] / "#{basename}.adoc"
166
+
167
+ erb = ERB.new(template_path.read, trim_mode: "-")
168
+ erb.filename = template_path.to_s
169
+
170
+ FileUtils.mkdir_p params[:output_dir]
171
+ File.write gen_filename, resolve_intermediate_links(cfg_arch, convert_monospace_to_links(cfg_arch, erb.result(binding)))
172
+
173
+ end
174
+
175
+ sig { void }
176
+ def gen_pdf
177
+ gen_adoc
178
+ adoc_filename = params[:output_dir] / "#{basename}.adoc"
179
+ pdf_filename = params[:output_dir] / "#{basename}.pdf"
180
+
181
+ Udb.logger.info "Running asciidoctor-pdf"
182
+ cmd = [
183
+ "asciidoctor-pdf",
184
+ "-w",
185
+ "-v",
186
+ "-a toc",
187
+ "-a compress",
188
+ "-a pdf-theme=#{params[:theme]}",
189
+ "-a pdf-fontsdir=#{params[:fonts]}",
190
+ "-a imagesdir=#{params[:images]}",
191
+ "-r asciidoctor-diagram",
192
+ "-r idl_highlighter",
193
+ "-a wavedrom=/opt/node/node_modules/.bin/wavedrom-cli",
194
+ "-o #{pdf_filename}",
195
+ adoc_filename
196
+ ].join(" ")
197
+
198
+ Udb.logger.debug cmd
199
+ system cmd
200
+
201
+ puts "SUCCESS! Wrote result to #{pdf_filename}"
202
+ end
203
+
204
+
205
+ sig { override.params(argv: T::Array[String]).returns(T.noreturn) }
206
+ def run(argv)
207
+ parse(argv)
208
+
209
+ if params[:help]
210
+ print help
211
+ exit_with(:success)
212
+ end
213
+
214
+ if params.errors.any?
215
+ exit_with(:usage_error, "#{params.errors.summary}\n\n#{help}")
216
+ end
217
+
218
+ unless params.remaining.empty?
219
+ exit_with(:usage_error, "Unknown arguments: #{params.remaining}\n")
220
+ end
221
+
222
+ gen_pdf
223
+
224
+ exit_with(:success)
225
+ end
226
+
227
+ end
228
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "sorbet-runtime"
8
+
9
+ module UdbGen
10
+ module ExtDocHelpers
11
+ extend T::Sig
12
+ include Kernel
13
+
14
+ # Returns the copyright year string for a version.
15
+ # Uses ratification_date year if available, otherwise current year.
16
+ sig { params(version: Udb::ExtensionVersion).returns(String) }
17
+ def copyright_year(version)
18
+ version.ratification_date.nil? ? Date.today.year.to_s : T.must(version.ratification_date).split("-")[0]
19
+ end
20
+
21
+ # Returns the revdate for a version (ratification date or today).
22
+ sig { params(version: Udb::ExtensionVersion).returns(T.any(String, Date)) }
23
+ def revdate(version)
24
+ version.ratification_date.nil? ? Date.today : version.ratification_date
25
+ end
26
+
27
+ # Returns the company name or "unknown".
28
+ sig { params(ext: Udb::Extension).returns(String) }
29
+ def company_name(ext)
30
+ ext.company.nil? ? "unknown" : T.must(ext.company).name
31
+ end
32
+
33
+ # Returns the doc license name or "unknown".
34
+ sig { params(ext: Udb::Extension).returns(String) }
35
+ def doc_license_name(ext)
36
+ ext.doc_license.nil? ? "unknown" : T.must(ext.doc_license).fetch("name")
37
+ end
38
+
39
+ # Returns the doc license URL or "unknown".
40
+ sig { params(ext: Udb::Extension).returns(String) }
41
+ def doc_license_url(ext)
42
+ ext.doc_license.nil? ? "unknown" : T.must(ext.doc_license).fetch("url")
43
+ end
44
+
45
+ # Returns true if the extension is RISC-V International branded.
46
+ sig { params(ext: Udb::Extension).returns(T::Boolean) }
47
+ def riscv_branded?(ext)
48
+ !ext.company.nil? && !(T.must(ext.company).name =~ /RISCV/).nil?
49
+ end
50
+
51
+ # Returns contributors for a version, sorted by last name.
52
+ sig { params(version: Udb::ExtensionVersion).returns(T::Array[Udb::Person]) }
53
+ def sorted_contributors(version)
54
+ version.contributors.sort { |a, b| T.must(a.name.split(" ").last <=> b.name.split(" ").last) }
55
+ end
56
+
57
+ # Returns all versions across all ext_reqs, flattened.
58
+ sig { params(ext_reqs: T::Array[Udb::ExtensionRequirement]).returns(T::Array[Udb::ExtensionVersion]) }
59
+ def all_versions(ext_reqs)
60
+ ext_reqs.map(&:satisfying_versions).flatten
61
+ end
62
+ end
63
+ end