nitpackage: check existence of README.md files
[nit.git] / src / nitpackage.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Helpful features about packages
16 module nitpackage
17
18 import frontend
19 import doc::commands::commands_main
20
21 redef class ToolContext
22
23 # nitpackage phase
24 var nitpackage_phase: Phase = new NitPackagePhase(self, null)
25
26 # --expand
27 var opt_expand = new OptionBool("Move singleton packages to their own directory", "--expand")
28
29 # --check-ini
30 var opt_check_ini = new OptionBool("Check package.ini files", "--check-ini")
31
32 # --gen-ini
33 var opt_gen_ini = new OptionBool("Generate package.ini files", "--gen-ini")
34
35 # --force
36 var opt_force = new OptionBool("Force update of existing files", "-f", "--force")
37
38 # --check-makefile
39 var opt_check_makefile = new OptionBool("Check Makefile files", "--check-makefile")
40
41 # --gen-makefile
42 var opt_gen_makefile = new OptionBool("Generate Makefile files", "--gen-makefile")
43
44 # --check-man
45 var opt_check_man = new OptionBool("Check manpages files", "--check-man")
46
47 # --gen-man
48 var opt_gen_man = new OptionBool("Generate manpages files", "--gen-man")
49
50 # --check-readme
51 var opt_check_readme = new OptionBool("Check README.md files", "--check-readme")
52
53 redef init do
54 super
55 option_context.add_option(opt_expand, opt_force)
56 option_context.add_option(opt_check_ini, opt_gen_ini)
57 option_context.add_option(opt_check_makefile, opt_gen_makefile)
58 option_context.add_option(opt_check_man, opt_gen_man)
59 option_context.add_option(opt_check_readme)
60 end
61 end
62
63 private class NitPackagePhase
64 super Phase
65
66 redef fun process_mainmodule(mainmodule, mmodules) do
67 var mpackages = extract_mpackages(mmodules)
68 for mpackage in mpackages do
69
70 # Fictive and buggy packages are ignored
71 if not mpackage.has_source then
72 toolcontext.warning(mpackage.location, "no-source",
73 "Warning: `{mpackage}` has no source file")
74 continue
75 end
76
77 # Check package INI files
78 if toolcontext.opt_check_ini.value then
79 mpackage.check_ini(toolcontext)
80 continue
81 end
82
83 # Check package Makefiles
84 if toolcontext.opt_check_makefile.value then
85 mpackage.check_makefile(toolcontext, mainmodule)
86 continue
87 end
88
89 # Check manpages
90 if toolcontext.opt_check_man.value then
91 mpackage.check_man(toolcontext, mainmodule)
92 continue
93 end
94
95 # Check README.md
96 if toolcontext.opt_check_readme.value then
97 mpackage.check_readme(toolcontext)
98 continue
99 end
100
101 # Expand packages
102 if toolcontext.opt_expand.value and not mpackage.is_expanded then
103 var path = mpackage.expand
104 toolcontext.info("{mpackage} moved to {path}", 0)
105 end
106 if not mpackage.is_expanded then
107 toolcontext.warning(mpackage.location, "no-dir",
108 "Warning: `{mpackage}` has no package directory")
109 continue
110 end
111
112 # Create INI file
113 if toolcontext.opt_gen_ini.value then
114 if not mpackage.has_ini or toolcontext.opt_force.value then
115 var path = mpackage.gen_ini
116 toolcontext.info("generated INI file `{path}`", 0)
117 end
118 end
119
120 # Create Makefile
121 if toolcontext.opt_gen_makefile.value then
122 if not mpackage.has_makefile or toolcontext.opt_force.value then
123 var path = mpackage.gen_makefile(toolcontext.modelbuilder.model, mainmodule)
124 if path != null then
125 toolcontext.info("generated Makefile `{path}`", 0)
126 end
127 end
128 end
129
130 # Create manpages
131 if toolcontext.opt_gen_man.value then
132 mpackage.gen_man(toolcontext, mainmodule)
133 end
134 end
135 end
136
137 # Extract the list of packages from the mmodules passed as arguments
138 fun extract_mpackages(mmodules: Collection[MModule]): Collection[MPackage] do
139 var mpackages = new ArraySet[MPackage]
140 for mmodule in mmodules do
141 var mpackage = mmodule.mpackage
142 if mpackage == null then continue
143 mpackages.add mpackage
144 end
145 return mpackages.to_a
146 end
147 end
148
149 redef class MPackage
150
151 # Expand `self` in its own directory
152 private fun expand: String do
153 assert not is_expanded
154
155 var ori_path = package_path.as(not null)
156 var new_path = ori_path.dirname / name
157
158 new_path.mkdir
159 sys.system "mv {ori_path} {new_path / name}.nit"
160
161 var ini_file = "{new_path}.ini"
162 if ini_file.file_exists then
163 sys.system "mv {new_path}.ini {new_path}/package.ini"
164 end
165
166 return new_path
167 end
168
169 private var maintainer: nullable String is lazy do
170 return git_exec("git shortlog -esn . | head -n 1 | sed 's/\\s*[0-9]*\\s*//'")
171 end
172
173 private var contributors: Array[String] is lazy do
174 var contribs = git_exec("git shortlog -esn . | head -n -1 | " +
175 "sed 's/\\s*[0-9]*\\s*//'")
176 if contribs == null then return new Array[String]
177 return contribs.split("\n")
178 end
179
180 private var git_url: nullable String is lazy do
181 var git = git_exec("git remote get-url origin")
182 if git == null then return null
183 git = git.replace("git@github.com:", "https://github.com/")
184 git = git.replace("git@gitlab.com:", "https://gitlab.com/")
185 return git
186 end
187
188 private var git_dir: nullable String is lazy do
189 return git_exec("git rev-parse --show-prefix")
190 end
191
192 private var browse_url: nullable String is lazy do
193 var git = git_url
194 if git == null then return null
195 var browse = git.replace(".git", "")
196 var dir = git_dir
197 if dir == null or dir.is_empty then return browse
198 return "{browse}/tree/master/{dir}"
199 end
200
201 private var homepage_url: nullable String is lazy do
202 var git = git_url
203 if git == null then return null
204 # Special case for nit files
205 if git.has_suffix("/nit.git") then
206 return "http://nitlanguage.org"
207 end
208 return git.replace(".git", "")
209 end
210
211 private var issues_url: nullable String is lazy do
212 var git = git_url
213 if git == null then return null
214 return "{git.replace(".git", "")}/issues"
215 end
216
217 private var license: nullable String is lazy do
218 var git = git_url
219 if git == null then return null
220 # Special case for nit files
221 if git.has_suffix("/nit.git") then
222 return "Apache-2.0"
223 end
224 return null
225 end
226
227 private fun git_exec(cmd: String): nullable String do
228 var path = package_path
229 if path == null then return null
230 if not is_expanded then path = path.dirname
231 with pr = new ProcessReader("sh", "-c", "cd {path} && {cmd}") do
232 return pr.read_all.trim
233 end
234 end
235
236 private var allowed_ini_keys = [
237 "package.name", "package.desc", "package.tags", "package.license",
238 "package.maintainer", "package.more_contributors",
239 "upstream.browse", "upstream.git", "upstream.git.directory",
240 "upstream.homepage", "upstream.issues", "upstream.apk", "upstream.tryit",
241 "source.exclude"
242 ]
243
244 private fun check_ini(toolcontext: ToolContext) do
245 if not has_ini then
246 toolcontext.error(location, "No `package.ini` file for `{name}`")
247 return
248 end
249
250 var pkg_path = package_path
251 if pkg_path == null then return
252
253 var ini_path = ini_path
254 if ini_path == null then return
255
256 var ini = new ConfigTree(ini_path)
257
258 ini.check_key(toolcontext, self, "package.name", name)
259 ini.check_key(toolcontext, self, "package.desc")
260 ini.check_key(toolcontext, self, "package.tags")
261
262 # FIXME since `git reflog --follow` seems bugged
263 ini.check_key(toolcontext, self, "package.maintainer")
264 # var maint = mpackage.maintainer
265 # if maint != null then
266 # ini.check_key(toolcontext, self, "package.maintainer", maint)
267 # end
268
269 # FIXME since `git reflog --follow` seems bugged
270 # var contribs = mpackage.contributors
271 # if contribs.not_empty then
272 # ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
273 # end
274
275 ini.check_key(toolcontext, self, "package.license", license)
276 ini.check_key(toolcontext, self, "upstream.browse", browse_url)
277 ini.check_key(toolcontext, self, "upstream.git", git_url)
278 ini.check_key(toolcontext, self, "upstream.git.directory", git_dir)
279 ini.check_key(toolcontext, self, "upstream.homepage", homepage_url)
280 ini.check_key(toolcontext, self, "upstream.issues", issues_url)
281
282 for key in ini.to_map.keys do
283 if not allowed_ini_keys.has(key) then
284 toolcontext.warning(location, "unknown-ini-key",
285 "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
286 end
287 end
288 end
289
290 private fun gen_ini: String do
291 var ini_path = self.ini_path.as(not null)
292 var ini = new ConfigTree(ini_path)
293
294 ini.update_value("package.name", name)
295 ini.update_value("package.desc", "")
296 ini.update_value("package.tags", "")
297 ini.update_value("package.maintainer", maintainer)
298 ini.update_value("package.more_contributors", contributors.join(","))
299 ini.update_value("package.license", license or else "")
300
301 ini.update_value("upstream.browse", browse_url)
302 ini.update_value("upstream.git", git_url)
303 ini.update_value("upstream.git.directory", git_dir)
304 ini.update_value("upstream.homepage", homepage_url)
305 ini.update_value("upstream.issues", issues_url)
306
307 ini.save
308 return ini_path
309 end
310
311 # Makefile
312
313 # The path to `self` Makefile
314 fun makefile_path: nullable String do
315 var path = package_path
316 if path == null then return null
317 if not is_expanded then return null
318 return path / "Makefile"
319 end
320
321 # Does `self` have a Makefile?
322 fun has_makefile: Bool do
323 var makefile_path = self.makefile_path
324 if makefile_path == null then return false
325 return makefile_path.file_exists
326 end
327
328 private fun check_makefile(toolcontext: ToolContext, mainmodule: MModule) do
329 var model = toolcontext.modelbuilder.model
330 var filter = new ModelFilter(accept_example = false, accept_test = false)
331 var view = new ModelView(model, mainmodule, filter)
332
333 var cmd_bin = new CmdMains(view, mentity = self)
334 var res_bin = cmd_bin.init_command
335 if not res_bin isa CmdSuccess then return
336
337 for mmodule in cmd_bin.results.as(not null) do
338 if not mmodule isa MModule then continue
339
340 if mmodule.makefile_path == null then
341 toolcontext.warning(location, "missing-makefile",
342 "Warning: no Makefile for executable module `{mmodule.full_name}`")
343 end
344 end
345 end
346
347 private fun gen_makefile(model: Model, mainmodule: MModule): nullable String do
348 var filter = new ModelFilter(accept_example = false, accept_test = false)
349 var view = new ModelView(model, mainmodule, filter)
350
351 var pkg_path = package_path.as(not null)
352 var makefile_path = makefile_path.as(not null)
353
354 var bins = new Array[String]
355 var cmd_bin = new CmdMains(view, mentity = self)
356 var res_bin = cmd_bin.init_command
357 if res_bin isa CmdSuccess then
358 for mmodule in cmd_bin.results.as(not null) do
359 if not mmodule isa MModule then continue
360 var mmodule_makefile = mmodule.makefile_path
361 if mmodule_makefile != null and mmodule_makefile != makefile_path then continue
362
363 var file = mmodule.location.file
364 if file == null then continue
365 # Remove package path prefix
366 var bin_path = file.filename
367 if pkg_path.has_suffix("/") then
368 bin_path = bin_path.replace(pkg_path, "")
369 else
370 bin_path = bin_path.replace("{pkg_path}/", "")
371 end
372 bins.add bin_path
373 end
374 end
375
376 if bins.is_empty then return null
377
378 var make = new NitMakefile(bins)
379 make.render.write_to_file(makefile_path)
380 return makefile_path
381 end
382
383 # Manpages
384
385 # The path to `self` manpage files
386 private fun man_path: nullable String do
387 var path = package_path
388 if path == null then return null
389 if not is_expanded then return null
390 return path / "man"
391 end
392
393 # Does `self` have a manpage files?
394 private fun has_man: Bool do
395 var man_path = self.man_path
396 if man_path == null then return false
397 return man_path.file_exists
398 end
399
400 private fun check_man(toolcontext: ToolContext, mainmodule: MModule) do
401 var model = toolcontext.modelbuilder.model
402 var filter = new ModelFilter(accept_example = false, accept_test = false)
403 var view = new ModelView(model, mainmodule, filter)
404
405 var cmd = new CmdMains(view, mentity = self)
406 var res = cmd.init_command
407 if not res isa CmdSuccess then return
408
409 for mmodule in cmd.results.as(not null) do
410 if not mmodule isa MModule then continue
411 mmodule.check_man(toolcontext)
412 end
413 end
414
415 private fun gen_man(toolcontext: ToolContext, mainmodule: MModule) do
416 var model = toolcontext.modelbuilder.model
417 var filter = new ModelFilter(accept_example = false, accept_test = false)
418 var view = new ModelView(model, mainmodule, filter)
419
420 var cmd = new CmdMains(view, mentity = self)
421 var res = cmd.init_command
422 if not res isa CmdSuccess then return
423
424 var pkg_man = man_path.as(not null)
425 for mmodule in cmd.results.as(not null) do
426 if not mmodule isa MModule then continue
427 if not has_man then pkg_man.mkdir
428 mmodule.gen_man(toolcontext)
429 end
430 end
431
432 # README
433
434 private fun check_readme(toolcontext: ToolContext) do
435 if not has_readme then
436 toolcontext.error(location, "No `README.md` file for `{name}`")
437 return
438 end
439 end
440 end
441
442 redef class MModule
443 private fun makefile_path: nullable String do
444 var file = location.file
445 if file == null then return null
446
447 var dir = file.filename.dirname
448 var makefile = (dir / "Makefile")
449 if not makefile.file_exists then return null
450
451 for line in makefile.to_path.read_lines do
452 if line.has_prefix("{name}:") then return makefile
453 end
454 return null
455 end
456
457 private fun man_path: nullable String do
458 var mpackage = self.mpackage
459 if mpackage == null then return null
460 var path = mpackage.man_path
461 if path == null then return null
462 return path / "{name}.man"
463 end
464
465 # Does `self` have a manpage?
466 private fun has_man: Bool do
467 var man_path = self.man_path
468 if man_path == null then return false
469 return man_path.file_exists
470 end
471
472 private fun make_module(toolcontext: ToolContext): Bool do
473 var mpackage = self.mpackage
474 if mpackage == null then return false
475 if not mpackage.is_expanded then return false
476
477 var pkg_path = mpackage.package_path
478 if pkg_path == null then return false
479
480 var pr = new ProcessReader("sh", "-c", "cd {pkg_path} && make -Bs bin/{name}")
481 var out = pr.read_all.trim
482 pr.close
483 pr.wait
484 if pr.status > 0 then
485 toolcontext.error(location, "unable to compile `{name}`")
486 print out
487 return false
488 end
489 return true
490 end
491
492 private fun stub_man(toolcontext: ToolContext): nullable String do
493 if not make_module(toolcontext) then return null
494 var mpackage = self.mpackage
495 if mpackage == null then return null
496 if not mpackage.is_expanded then return null
497
498 var pkg_path = mpackage.package_path
499 if pkg_path == null then return null
500
501 var pr = new ProcessReader("{pkg_path}/bin/{name}", "--stub-man")
502 var man = pr.read_all.trim
503 pr.close
504 pr.wait
505 if pr.status > 0 then
506 toolcontext.error(location, "unable to run `{pkg_path}/bin/{name} --stub-man`")
507 print man
508 return null
509 end
510 return man
511 end
512
513 private fun check_man(toolcontext: ToolContext) do
514 if not has_man then
515 toolcontext.error(location, "No manpage for bin {full_name}")
516 return
517 end
518 var man_path = self.man_path.as(not null)
519 var man = stub_man(toolcontext)
520 if man == null or man.is_empty then return
521
522 var old_man = new ManPage.from_file(self, man_path)
523 var new_man = new ManPage.from_string(self, man)
524 old_man.diff(toolcontext, new_man)
525 end
526
527 private fun gen_man(toolcontext: ToolContext) do
528 var man = stub_man(toolcontext)
529 if man == null or man.is_empty then return
530 var man_path = self.man_path
531 if man_path == null then return
532 man.write_to_file(man_path)
533 toolcontext.info("created manpage `{man_path}`", 0)
534 end
535 end
536
537 redef class ConfigTree
538 private fun check_key(toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
539 if not has_key(key) then
540 toolcontext.warning(mpackage.location, "missing-ini-key",
541 "Warning: missing `{key}` key in `{ini_file}`")
542 return
543 end
544 if self[key].as(not null).is_empty then
545 toolcontext.warning(mpackage.location, "missing-ini-value",
546 "Warning: empty `{key}` key in `{ini_file}`")
547 return
548 end
549 if value != null and self[key] != value then
550 toolcontext.warning(mpackage.location, "wrong-ini-value",
551 "Warning: wrong value for `{key}` in `{ini_file}`. " +
552 "Expected `{value}`, got `{self[key] or else ""}`")
553 end
554 end
555
556 private fun update_value(key: String, value: nullable String) do
557 if value == null then return
558 if not has_key(key) then
559 self[key] = value
560 else
561 var old_value = self[key]
562 if not value.is_empty and old_value != value then
563 self[key] = value
564 end
565 end
566 end
567 end
568
569 # A Makefile for the Nit project
570 class NitMakefile
571
572 # Nit files to compile
573 var nit_files: Array[String]
574
575 # List of rules to add in the Makefile
576 fun rules: Array[MakeRule] do
577 var rules = new Array[MakeRule]
578
579 var rule_all = new MakeRule("all", is_phony = true)
580 rules.add rule_all
581
582 for file in nit_files do
583 var bin = file.basename.strip_extension
584
585 rule_all.deps.add "bin/{bin}"
586
587 var rule = new MakeRule("bin/{bin}")
588 rule.deps.add "$(shell $(NITLS) -M {file})"
589 rule.lines.add "mkdir -p bin/"
590 rule.lines.add "$(NITC) {file} -o bin/{bin}"
591 rules.add rule
592 end
593
594 var rule_check = new MakeRule("check", is_phony = true)
595 rule_check.lines.add "$(NITUNIT) ."
596 rules.add rule_check
597
598 var rule_doc = new MakeRule("doc", is_phony = true)
599 rule_doc.lines.add "$(NITDOC) . -o doc/"
600 rules.add rule_doc
601
602 var rule_clean = new MakeRule("clean", is_phony = true)
603 if nit_files.not_empty then
604 rule_clean.lines.add "rm -rf bin/"
605 end
606 rule_clean.lines.add "rm -rf doc/"
607 rules.add rule_clean
608
609 return rules
610 end
611
612 # Render `self`
613 fun render: Writable do
614 var tpl = new Template
615 tpl.addn """
616 # This file is part of NIT ( http://www.nitlanguage.org ).
617 #
618 # Licensed under the Apache License, Version 2.0 (the "License");
619 # you may not use this file except in compliance with the License.
620 # You may obtain a copy of the License at
621 #
622 # http://www.apache.org/licenses/LICENSE-2.0
623 #
624 # Unless required by applicable law or agreed to in writing, software
625 # distributed under the License is distributed on an "AS IS" BASIS,
626 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
627 # See the License for the specific language governing permissions and
628 # limitations under the License.\n"""
629
630 if nit_files.not_empty then
631 tpl.addn "NITC ?= nitc"
632 tpl.addn "NITLS ?= nitls"
633 end
634 tpl.addn "NITUNIT ?= nitunit"
635 tpl.addn "NITDOC ?= nitdoc"
636
637 for rule in rules do
638 tpl.add "\n{rule.render.write_to_string}"
639 end
640
641 return tpl
642 end
643 end
644
645 # A rule that goes into a Makefile
646 class MakeRule
647
648 # Rule name
649 var name: String
650
651 # Is this rule a `.PHONY` one?
652 var is_phony: Bool = false is optional
653
654 # Rule dependencies
655 var deps = new Array[String]
656
657 # Rule lines
658 var lines = new Array[String]
659
660 # Render `self`
661 fun render: Writable do
662 var tpl = new Template
663 if is_phony then
664 tpl.addn ".PHONY: {name}"
665 end
666 tpl.add "{name}:"
667 if deps.not_empty then
668 tpl.add " {deps.join(" ")}"
669 end
670 tpl.add "\n"
671 for line in lines do
672 tpl.addn "\t{line}"
673 end
674 return tpl
675 end
676 end
677
678 private class ManPage
679 var mmodule: MModule
680 var name: nullable String is noinit
681 var synopsis: nullable String is noinit
682 var options = new HashMap[Array[String], String]
683
684 init from_file(mmodule: MModule, file: String) do
685 from_lines(mmodule, file.to_path.read_lines)
686 end
687
688 init from_string(mmodule: MModule, string: String) do
689 from_lines(mmodule, string.split("\n"))
690 end
691
692 init from_lines(mmodule: MModule, lines: Array[String]) do
693 init mmodule
694
695 var section = null
696 for i in [0..lines.length[ do
697 var line = lines[i]
698 if line.is_empty then continue
699
700 if line == "# NAME" then
701 section = "name"
702 continue
703 end
704 if line == "# SYNOPSIS" then
705 section = "synopsis"
706 continue
707 end
708 if line == "# OPTIONS" then
709 section = "options"
710 continue
711 end
712
713 if section == "name" and name == null then
714 name = line.trim
715 end
716 if section == "synopsis" and synopsis == null then
717 synopsis = line.trim
718 end
719 if section == "options" and line.has_prefix("###") then
720 var opts = new Array[String]
721 for opt in line.substring(3, line.length).trim.replace("`", "").split(",") do
722 opts.add opt.trim
723 end
724 var desc = ""
725 if i < lines.length - 1 then
726 desc = lines[i + 1].trim
727 end
728 options[opts] = desc
729 end
730 end
731 end
732
733 fun diff(toolcontext: ToolContext, ref: ManPage) do
734 if name != ref.name then
735 toolcontext.warning(mmodule.location, "diff-man",
736 "Warning: outdated man description. " +
737 "Expected `{ref.name or else ""}` got `{name or else ""}`.")
738 end
739 if synopsis != ref.synopsis then
740 toolcontext.warning(mmodule.location, "diff-man",
741 "Warning: outdated man synopsis. " +
742 "Expected `{ref.synopsis or else ""}` got `{synopsis or else ""}`.")
743 end
744 for name, desc in options do
745 if not ref.options.has_key(name) then
746 toolcontext.warning(mmodule.location, "diff-man",
747 "Warning: unknown man option `{name}`.`")
748 continue
749 end
750 var ref_desc = ref.options[name]
751 if desc != ref_desc then
752 toolcontext.warning(mmodule.location, "diff-man",
753 "Warning: outdated man option description. Expected `{ref_desc}` got `{desc}`.")
754 end
755 end
756 for ref_name, ref_desc in ref.options do
757 if not options.has_key(ref_name) then
758 toolcontext.warning(mmodule.location, "diff-man",
759 "Warning: missing man option `{ref_name}`.`")
760 end
761 end
762 end
763
764 redef fun to_s do
765 var tpl = new Template
766 tpl.addn "# NAME"
767 tpl.addn name or else ""
768 tpl.addn "# SYNOPSIS"
769 tpl.addn synopsis or else ""
770 tpl.addn "# OPTIONS"
771 for name, desc in options do
772 tpl.addn " * {name}: {desc}"
773 end
774 return tpl.write_to_string
775 end
776 end
777
778 # build toolcontext
779 var toolcontext = new ToolContext
780 var tpl = new Template
781 tpl.add "Usage: nitpackage [OPTION]... <file.nit>...\n"
782 tpl.add "Helpful features about packages."
783 toolcontext.tooldescription = tpl.write_to_string
784
785 # process options
786 toolcontext.process_options(args)
787 var arguments = toolcontext.option_context.rest
788
789 # build model
790 var model = new Model
791 var mbuilder = new ModelBuilder(model, toolcontext)
792 var mmodules = mbuilder.parse_full(arguments)
793
794 # process
795 if mmodules.is_empty then return
796 mbuilder.run_phases
797 toolcontext.run_global_phases(mmodules)