1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Helpful features about packages
19 import doc
::commands
::commands_main
21 redef class ToolContext
24 var nitpackage_phase
: Phase = new NitPackagePhase(self, null)
27 var opt_expand
= new OptionBool("Move singleton packages to their own directory", "--expand")
30 var opt_check_ini
= new OptionBool("Check package.ini files", "--check-ini")
33 var opt_gen_ini
= new OptionBool("Generate package.ini files", "--gen-ini")
36 var opt_force
= new OptionBool("Force update of existing files", "-f", "--force")
39 var opt_check_makefile
= new OptionBool("Check Makefile files", "--check-makefile")
42 var opt_gen_makefile
= new OptionBool("Generate Makefile files", "--gen-makefile")
45 var opt_check_man
= new OptionBool("Check manpages files", "--check-man")
48 var opt_gen_man
= new OptionBool("Generate manpages files", "--gen-man")
51 var opt_check_readme
= new OptionBool("Check README.md files", "--check-readme")
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
)
63 private class NitPackagePhase
66 redef fun process_mainmodule
(mainmodule
, mmodules
) do
67 var mpackages
= extract_mpackages
(mmodules
)
68 for mpackage
in mpackages
do
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")
77 # Check package INI files
78 if toolcontext
.opt_check_ini
.value
then
79 mpackage
.check_ini
(toolcontext
)
83 # Check package Makefiles
84 if toolcontext
.opt_check_makefile
.value
then
85 mpackage
.check_makefile
(toolcontext
, mainmodule
)
90 if toolcontext
.opt_check_man
.value
then
91 mpackage
.check_man
(toolcontext
, mainmodule
)
96 if toolcontext
.opt_check_readme
.value
then
97 mpackage
.check_readme
(toolcontext
)
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)
106 if not mpackage
.is_expanded
then
107 toolcontext
.warning
(mpackage
.location
, "no-dir",
108 "Warning: `{mpackage}` has no package directory")
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)
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
)
125 toolcontext
.info
("generated Makefile `{path}`", 0)
131 if toolcontext
.opt_gen_man
.value
then
132 mpackage
.gen_man
(toolcontext
, mainmodule
)
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
145 return mpackages
.to_a
151 # Expand `self` in its own directory
152 private fun expand
: String do
153 assert not is_expanded
155 var ori_path
= package_path
.as(not null)
156 var new_path
= ori_path
.dirname
/ name
159 sys
.system
"mv {ori_path} {new_path / name}.nit"
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"
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*//'")
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")
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/")
188 private var git_dir
: nullable String is lazy
do
189 return git_exec
("git rev-parse --show-prefix")
192 private var browse_url
: nullable String is lazy
do
194 if git
== null then return null
195 var browse
= git
.replace
(".git", "")
197 if dir
== null or dir
.is_empty
then return browse
198 return "{browse}/tree/master/{dir}"
201 private var homepage_url
: nullable String is lazy
do
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"
208 return git
.replace
(".git", "")
211 private var issues_url
: nullable String is lazy
do
213 if git
== null then return null
214 return "{git.replace(".git", "")}/issues"
217 private var license
: nullable String is lazy
do
219 if git
== null then return null
220 # Special case for nit files
221 if git
.has_suffix
("/nit.git") then
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
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",
244 private fun check_ini
(toolcontext
: ToolContext) do
246 toolcontext
.error
(location
, "No `package.ini` file for `{name}`")
250 var pkg_path
= package_path
251 if pkg_path
== null then return
253 var ini_path
= ini_path
254 if ini_path
== null then return
256 var ini
= new ConfigTree(ini_path
)
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")
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)
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(", "))
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
)
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}`")
290 private fun gen_ini
: String do
291 var ini_path
= self.ini_path
.as(not null)
292 var ini
= new ConfigTree(ini_path
)
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 "")
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
)
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"
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
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
)
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
337 for mmodule
in cmd_bin
.results
.as(not null) do
338 if not mmodule
isa MModule then continue
340 if mmodule
.makefile_path
== null then
341 toolcontext
.warning
(location
, "missing-makefile",
342 "Warning: no Makefile for executable module `{mmodule.full_name}`")
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
)
351 var pkg_path
= package_path
.as(not null)
352 var makefile_path
= makefile_path
.as(not null)
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
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
, "")
370 bin_path
= bin_path
.replace
("{pkg_path}/", "")
376 if bins
.is_empty
then return null
378 var make
= new NitMakefile(bins
)
379 make
.render
.write_to_file
(makefile_path
)
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
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
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
)
405 var cmd
= new CmdMains(view
, mentity
= self)
406 var res
= cmd
.init_command
407 if not res
isa CmdSuccess then return
409 for mmodule
in cmd
.results
.as(not null) do
410 if not mmodule
isa MModule then continue
411 mmodule
.check_man
(toolcontext
)
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
)
420 var cmd
= new CmdMains(view
, mentity
= self)
421 var res
= cmd
.init_command
422 if not res
isa CmdSuccess then return
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
)
434 private fun check_readme
(toolcontext
: ToolContext) do
435 if not has_readme
then
436 toolcontext
.error
(location
, "No `README.md` file for `{name}`")
443 private fun makefile_path
: nullable String do
444 var file
= location
.file
445 if file
== null then return null
447 var dir
= file
.filename
.dirname
448 var makefile
= (dir
/ "Makefile")
449 if not makefile
.file_exists
then return null
451 for line
in makefile
.to_path
.read_lines
do
452 if line
.has_prefix
("{name}:") then return makefile
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"
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
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
477 var pkg_path
= mpackage
.package_path
478 if pkg_path
== null then return false
480 var pr
= new ProcessReader("sh", "-c", "cd {pkg_path} && make -Bs bin/{name}")
481 var out
= pr
.read_all
.trim
484 if pr
.status
> 0 then
485 toolcontext
.error
(location
, "unable to compile `{name}`")
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
498 var pkg_path
= mpackage
.package_path
499 if pkg_path
== null then return null
501 var pr
= new ProcessReader("{pkg_path}/bin/{name}", "--stub-man")
502 var man
= pr
.read_all
.trim
505 if pr
.status
> 0 then
506 toolcontext
.error
(location
, "unable to run `{pkg_path}/bin/{name} --stub-man`")
513 private fun check_man
(toolcontext
: ToolContext) do
515 toolcontext
.error
(location
, "No manpage for bin {full_name}")
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
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
)
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)
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}`")
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}`")
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 ""}`")
556 private fun update_value
(key
: String, value
: nullable String) do
557 if value
== null then return
558 if not has_key
(key
) then
561 var old_value
= self[key
]
562 if not value
.is_empty
and old_value
!= value
then
569 # A Makefile for the Nit project
572 # Nit files to compile
573 var nit_files
: Array[String]
575 # List of rules to add in the Makefile
576 fun rules
: Array[MakeRule] do
577 var rules
= new Array[MakeRule]
579 var rule_all
= new MakeRule("all", is_phony
= true)
582 for file
in nit_files
do
583 var bin
= file
.basename
.strip_extension
585 rule_all
.deps
.add
"bin/{bin}"
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}"
594 var rule_check
= new MakeRule("check", is_phony
= true)
595 rule_check
.lines
.add
"$(NITUNIT) ."
598 var rule_doc
= new MakeRule("doc", is_phony
= true)
599 rule_doc
.lines
.add
"$(NITDOC) . -o doc/"
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/"
606 rule_clean
.lines
.add
"rm -rf doc/"
613 fun render
: Writable do
614 var tpl
= new Template
616 # This file is part of NIT ( http://www.nitlanguage.org ).
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
622 # http://www.apache.org/licenses/LICENSE-2.0
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"""
630 if nit_files
.not_empty
then
631 tpl
.addn
"NITC ?= nitc"
632 tpl
.addn
"NITLS ?= nitls"
634 tpl
.addn
"NITUNIT ?= nitunit"
635 tpl
.addn
"NITDOC ?= nitdoc"
638 tpl
.add
"\n{rule.render.write_to_string}"
645 # A rule that goes into a Makefile
651 # Is this rule a `.PHONY` one?
652 var is_phony
: Bool = false is optional
655 var deps
= new Array[String]
658 var lines
= new Array[String]
661 fun render
: Writable do
662 var tpl
= new Template
664 tpl
.addn
".PHONY: {name}"
667 if deps
.not_empty
then
668 tpl
.add
" {deps.join(" ")}"
678 private class ManPage
680 var name
: nullable String is noinit
681 var synopsis
: nullable String is noinit
682 var options
= new HashMap[Array[String], String]
684 init from_file
(mmodule
: MModule, file
: String) do
685 from_lines
(mmodule
, file
.to_path
.read_lines
)
688 init from_string
(mmodule
: MModule, string
: String) do
689 from_lines
(mmodule
, string
.split
("\n"))
692 init from_lines
(mmodule
: MModule, lines
: Array[String]) do
696 for i
in [0..lines
.length
[ do
698 if line
.is_empty
then continue
700 if line
== "# NAME" then
704 if line
== "# SYNOPSIS" then
708 if line
== "# OPTIONS" then
713 if section
== "name" and name
== null then
716 if section
== "synopsis" and synopsis
== null then
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
725 if i
< lines
.length
- 1 then
726 desc
= lines
[i
+ 1].trim
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 ""}`.")
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 ""}`.")
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}`.`")
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}`.")
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}`.`")
765 var tpl
= new Template
767 tpl
.addn name
or else ""
768 tpl
.addn
"# SYNOPSIS"
769 tpl
.addn synopsis
or else ""
771 for name
, desc
in options
do
772 tpl
.addn
" * {name}: {desc}"
774 return tpl
.write_to_string
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
786 toolcontext
.process_options
(args
)
787 var arguments
= toolcontext
.option_context
.rest
790 var model
= new Model
791 var mbuilder
= new ModelBuilder(model
, toolcontext
)
792 var mmodules
= mbuilder
.parse_full
(arguments
)
795 if mmodules
.is_empty
then return
797 toolcontext
.run_global_phases
(mmodules
)