module nitpackage
import frontend
+import doc::commands::commands_main
redef class ToolContext
# --expand
var opt_expand = new OptionBool("Move singleton packages to their own directory", "--expand")
+ # --check-ini
+ var opt_check_ini = new OptionBool("Check package.ini files", "--check-ini")
+
# --gen-ini
var opt_gen_ini = new OptionBool("Generate package.ini files", "--gen-ini")
# --force
var opt_force = new OptionBool("Force update of existing files", "-f", "--force")
- # README handling phase
- var readme_phase: Phase = new ReadmePhase(self, null)
+ # --check-makefile
+ var opt_check_makefile = new OptionBool("Check Makefile files", "--check-makefile")
+
+ # --gen-makefile
+ var opt_gen_makefile = new OptionBool("Generate Makefile files", "--gen-makefile")
+
+ # nitpackage phase
+ var nitpackage_phase: Phase = new NitPackagePhase(self, null)
+
+ # --check-man
+ var opt_check_man = new OptionBool("Check manpages files", "--check-man")
+
+ # --gen-man
+ var opt_gen_man = new OptionBool("Generate manpages files", "--gen-man")
redef init do
super
option_context.add_option(opt_expand, opt_force)
- option_context.add_option(opt_gen_ini)
+ option_context.add_option(opt_check_ini, opt_gen_ini)
+ option_context.add_option(opt_check_makefile, opt_gen_makefile)
+ option_context.add_option(opt_check_man, opt_gen_man)
end
end
-private class ReadmePhase
+private class NitPackagePhase
super Phase
redef fun process_mainmodule(mainmodule, mmodules) do
continue
end
+ # Check package INI files
+ if toolcontext.opt_check_ini.value then
+ mpackage.check_ini(toolcontext)
+ continue
+ end
+
+ # Check package Makefiles
+ if toolcontext.opt_check_makefile.value then
+ mpackage.check_makefile(toolcontext, mainmodule)
+ continue
+ end
+
+ # Check manpages
+ if toolcontext.opt_check_man.value then
+ mpackage.check_man(toolcontext, mainmodule)
+ continue
+ end
+
# Expand packages
if toolcontext.opt_expand.value and not mpackage.is_expanded then
var path = mpackage.expand
toolcontext.info("generated INI file `{path}`", 0)
end
end
+
+ # Create Makefile
+ if toolcontext.opt_gen_makefile.value then
+ if not mpackage.has_makefile or toolcontext.opt_force.value then
+ var path = mpackage.gen_makefile(toolcontext.modelbuilder.model, mainmodule)
+ if path != null then
+ toolcontext.info("generated Makefile `{path}`", 0)
+ end
+ end
+ end
+
+ # Create manpages
+ if toolcontext.opt_gen_man.value then
+ mpackage.gen_man(toolcontext, mainmodule)
+ end
end
end
end
end
+ private var allowed_ini_keys = [
+ "package.name", "package.desc", "package.tags", "package.license",
+ "package.maintainer", "package.more_contributors",
+ "upstream.browse", "upstream.git", "upstream.git.directory",
+ "upstream.homepage", "upstream.issues", "upstream.apk", "upstream.tryit",
+ "source.exclude"
+ ]
+
+ private fun check_ini(toolcontext: ToolContext) do
+ if not has_ini then
+ toolcontext.error(location, "No `package.ini` file for `{name}`")
+ return
+ end
+
+ var pkg_path = package_path
+ if pkg_path == null then return
+
+ var ini_path = ini_path
+ if ini_path == null then return
+
+ var ini = new ConfigTree(ini_path)
+
+ ini.check_key(toolcontext, self, "package.name", name)
+ ini.check_key(toolcontext, self, "package.desc")
+ ini.check_key(toolcontext, self, "package.tags")
+
+ # FIXME since `git reflog --follow` seems bugged
+ ini.check_key(toolcontext, self, "package.maintainer")
+ # var maint = mpackage.maintainer
+ # if maint != null then
+ # ini.check_key(toolcontext, self, "package.maintainer", maint)
+ # end
+
+ # FIXME since `git reflog --follow` seems bugged
+ # var contribs = mpackage.contributors
+ # if contribs.not_empty then
+ # ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
+ # end
+
+ ini.check_key(toolcontext, self, "package.license", license)
+ ini.check_key(toolcontext, self, "upstream.browse", browse_url)
+ ini.check_key(toolcontext, self, "upstream.git", git_url)
+ ini.check_key(toolcontext, self, "upstream.git.directory", git_dir)
+ ini.check_key(toolcontext, self, "upstream.homepage", homepage_url)
+ ini.check_key(toolcontext, self, "upstream.issues", issues_url)
+
+ for key in ini.to_map.keys do
+ if not allowed_ini_keys.has(key) then
+ toolcontext.warning(location, "unknown-ini-key",
+ "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
+ end
+ end
+ end
+
private fun gen_ini: String do
var ini_path = self.ini_path.as(not null)
var ini = new ConfigTree(ini_path)
ini.save
return ini_path
end
+
+ # Makefile
+
+ # The path to `self` Makefile
+ fun makefile_path: nullable String do
+ var path = package_path
+ if path == null then return null
+ if not is_expanded then return null
+ return path / "Makefile"
+ end
+
+ # Does `self` have a Makefile?
+ fun has_makefile: Bool do
+ var makefile_path = self.makefile_path
+ if makefile_path == null then return false
+ return makefile_path.file_exists
+ end
+
+ private fun check_makefile(toolcontext: ToolContext, mainmodule: MModule) do
+ var model = toolcontext.modelbuilder.model
+ var filter = new ModelFilter(accept_example = false, accept_test = false)
+ var view = new ModelView(model, mainmodule, filter)
+
+ var cmd_bin = new CmdMains(view, mentity = self)
+ var res_bin = cmd_bin.init_command
+ if not res_bin isa CmdSuccess then return
+
+ for mmodule in cmd_bin.results.as(not null) do
+ if not mmodule isa MModule then continue
+
+ if mmodule.makefile_path == null then
+ toolcontext.warning(location, "missing-makefile",
+ "Warning: no Makefile for executable module `{mmodule.full_name}`")
+ end
+ end
+ end
+
+ private fun gen_makefile(model: Model, mainmodule: MModule): nullable String do
+ var filter = new ModelFilter(accept_example = false, accept_test = false)
+ var view = new ModelView(model, mainmodule, filter)
+
+ var pkg_path = package_path.as(not null)
+ var makefile_path = makefile_path.as(not null)
+
+ var bins = new Array[String]
+ var cmd_bin = new CmdMains(view, mentity = self)
+ var res_bin = cmd_bin.init_command
+ if res_bin isa CmdSuccess then
+ for mmodule in cmd_bin.results.as(not null) do
+ if not mmodule isa MModule then continue
+ var mmodule_makefile = mmodule.makefile_path
+ if mmodule_makefile != null and mmodule_makefile != makefile_path then continue
+
+ var file = mmodule.location.file
+ if file == null then continue
+ # Remove package path prefix
+ var bin_path = file.filename
+ if pkg_path.has_suffix("/") then
+ bin_path = bin_path.replace(pkg_path, "")
+ else
+ bin_path = bin_path.replace("{pkg_path}/", "")
+ end
+ bins.add bin_path
+ end
+ end
+
+ if bins.is_empty then return null
+
+ var make = new NitMakefile(bins)
+ make.render.write_to_file(makefile_path)
+ return makefile_path
+ end
+
+ # Manpages
+
+ # The path to `self` manpage files
+ private fun man_path: nullable String do
+ var path = package_path
+ if path == null then return null
+ if not is_expanded then return null
+ return path / "man"
+ end
+
+ # Does `self` have a manpage files?
+ private fun has_man: Bool do
+ var man_path = self.man_path
+ if man_path == null then return false
+ return man_path.file_exists
+ end
+
+ private fun check_man(toolcontext: ToolContext, mainmodule: MModule) do
+ var model = toolcontext.modelbuilder.model
+ var filter = new ModelFilter(accept_example = false, accept_test = false)
+ var view = new ModelView(model, mainmodule, filter)
+
+ var cmd = new CmdMains(view, mentity = self)
+ var res = cmd.init_command
+ if not res isa CmdSuccess then return
+
+ for mmodule in cmd.results.as(not null) do
+ if not mmodule isa MModule then continue
+ mmodule.check_man(toolcontext)
+ end
+ end
+
+ private fun gen_man(toolcontext: ToolContext, mainmodule: MModule) do
+ var model = toolcontext.modelbuilder.model
+ var filter = new ModelFilter(accept_example = false, accept_test = false)
+ var view = new ModelView(model, mainmodule, filter)
+
+ var cmd = new CmdMains(view, mentity = self)
+ var res = cmd.init_command
+ if not res isa CmdSuccess then return
+
+ var pkg_man = man_path.as(not null)
+ for mmodule in cmd.results.as(not null) do
+ if not mmodule isa MModule then continue
+ if not has_man then pkg_man.mkdir
+ mmodule.gen_man(toolcontext)
+ end
+ end
+end
+
+redef class MModule
+ private fun makefile_path: nullable String do
+ var file = location.file
+ if file == null then return null
+
+ var dir = file.filename.dirname
+ var makefile = (dir / "Makefile")
+ if not makefile.file_exists then return null
+
+ for line in makefile.to_path.read_lines do
+ if line.has_prefix("{name}:") then return makefile
+ end
+ return null
+ end
+
+ private fun man_path: nullable String do
+ var mpackage = self.mpackage
+ if mpackage == null then return null
+ var path = mpackage.man_path
+ if path == null then return null
+ return path / "{name}.man"
+ end
+
+ # Does `self` have a manpage?
+ private fun has_man: Bool do
+ var man_path = self.man_path
+ if man_path == null then return false
+ return man_path.file_exists
+ end
+
+ private fun make_module(toolcontext: ToolContext): Bool do
+ var mpackage = self.mpackage
+ if mpackage == null then return false
+ if not mpackage.is_expanded then return false
+
+ var pkg_path = mpackage.package_path
+ if pkg_path == null then return false
+
+ var pr = new ProcessReader("sh", "-c", "cd {pkg_path} && make -Bs bin/{name}")
+ var out = pr.read_all.trim
+ pr.close
+ pr.wait
+ if pr.status > 0 then
+ toolcontext.error(location, "unable to compile `{name}`")
+ print out
+ return false
+ end
+ return true
+ end
+
+ private fun stub_man(toolcontext: ToolContext): nullable String do
+ if not make_module(toolcontext) then return null
+ var mpackage = self.mpackage
+ if mpackage == null then return null
+ if not mpackage.is_expanded then return null
+
+ var pkg_path = mpackage.package_path
+ if pkg_path == null then return null
+
+ var pr = new ProcessReader("{pkg_path}/bin/{name}", "--stub-man")
+ var man = pr.read_all.trim
+ pr.close
+ pr.wait
+ if pr.status > 0 then
+ toolcontext.error(location, "unable to run `{pkg_path}/bin/{name} --stub-man`")
+ print man
+ return null
+ end
+ return man
+ end
+
+ private fun check_man(toolcontext: ToolContext) do
+ if not has_man then
+ toolcontext.error(location, "No manpage for bin {full_name}")
+ return
+ end
+ var man_path = self.man_path.as(not null)
+ var man = stub_man(toolcontext)
+ if man == null or man.is_empty then return
+
+ var old_man = new ManPage.from_file(self, man_path)
+ var new_man = new ManPage.from_string(self, man)
+ old_man.diff(toolcontext, new_man)
+ end
+
+ private fun gen_man(toolcontext: ToolContext) do
+ var man = stub_man(toolcontext)
+ if man == null or man.is_empty then return
+ var man_path = self.man_path
+ if man_path == null then return
+ man.write_to_file(man_path)
+ toolcontext.info("created manpage `{man_path}`", 0)
+ end
end
redef class ConfigTree
+ private fun check_key(toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
+ if not has_key(key) then
+ toolcontext.warning(mpackage.location, "missing-ini-key",
+ "Warning: missing `{key}` key in `{ini_file}`")
+ return
+ end
+ if self[key].as(not null).is_empty then
+ toolcontext.warning(mpackage.location, "missing-ini-value",
+ "Warning: empty `{key}` key in `{ini_file}`")
+ return
+ end
+ if value != null and self[key] != value then
+ toolcontext.warning(mpackage.location, "wrong-ini-value",
+ "Warning: wrong value for `{key}` in `{ini_file}`. " +
+ "Expected `{value}`, got `{self[key] or else ""}`")
+ end
+ end
+
private fun update_value(key: String, value: nullable String) do
if value == null then return
if not has_key(key) then
end
end
+# A Makefile for the Nit project
+class NitMakefile
+
+ # Nit files to compile
+ var nit_files: Array[String]
+
+ # List of rules to add in the Makefile
+ fun rules: Array[MakeRule] do
+ var rules = new Array[MakeRule]
+
+ var rule_all = new MakeRule("all", is_phony = true)
+ rules.add rule_all
+
+ for file in nit_files do
+ var bin = file.basename.strip_extension
+
+ rule_all.deps.add "bin/{bin}"
+
+ var rule = new MakeRule("bin/{bin}")
+ rule.deps.add "$(shell $(NITLS) -M {file})"
+ rule.lines.add "mkdir -p bin/"
+ rule.lines.add "$(NITC) {file} -o bin/{bin}"
+ rules.add rule
+ end
+
+ var rule_check = new MakeRule("check", is_phony = true)
+ rule_check.lines.add "$(NITUNIT) ."
+ rules.add rule_check
+
+ var rule_doc = new MakeRule("doc", is_phony = true)
+ rule_doc.lines.add "$(NITDOC) . -o doc/"
+ rules.add rule_doc
+
+ var rule_clean = new MakeRule("clean", is_phony = true)
+ if nit_files.not_empty then
+ rule_clean.lines.add "rm -rf bin/"
+ end
+ rule_clean.lines.add "rm -rf doc/"
+ rules.add rule_clean
+
+ return rules
+ end
+
+ # Render `self`
+ fun render: Writable do
+ var tpl = new Template
+ tpl.addn """
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.\n"""
+
+ if nit_files.not_empty then
+ tpl.addn "NITC ?= nitc"
+ tpl.addn "NITLS ?= nitls"
+ end
+ tpl.addn "NITUNIT ?= nitunit"
+ tpl.addn "NITDOC ?= nitdoc"
+
+ for rule in rules do
+ tpl.add "\n{rule.render.write_to_string}"
+ end
+
+ return tpl
+ end
+end
+
+# A rule that goes into a Makefile
+class MakeRule
+
+ # Rule name
+ var name: String
+
+ # Is this rule a `.PHONY` one?
+ var is_phony: Bool = false is optional
+
+ # Rule dependencies
+ var deps = new Array[String]
+
+ # Rule lines
+ var lines = new Array[String]
+
+ # Render `self`
+ fun render: Writable do
+ var tpl = new Template
+ if is_phony then
+ tpl.addn ".PHONY: {name}"
+ end
+ tpl.add "{name}:"
+ if deps.not_empty then
+ tpl.add " {deps.join(" ")}"
+ end
+ tpl.add "\n"
+ for line in lines do
+ tpl.addn "\t{line}"
+ end
+ return tpl
+ end
+end
+
+private class ManPage
+ var mmodule: MModule
+ var name: nullable String is noinit
+ var synopsis: nullable String is noinit
+ var options = new HashMap[Array[String], String]
+
+ init from_file(mmodule: MModule, file: String) do
+ from_lines(mmodule, file.to_path.read_lines)
+ end
+
+ init from_string(mmodule: MModule, string: String) do
+ from_lines(mmodule, string.split("\n"))
+ end
+
+ init from_lines(mmodule: MModule, lines: Array[String]) do
+ init mmodule
+
+ var section = null
+ for i in [0..lines.length[ do
+ var line = lines[i]
+ if line.is_empty then continue
+
+ if line == "# NAME" then
+ section = "name"
+ continue
+ end
+ if line == "# SYNOPSIS" then
+ section = "synopsis"
+ continue
+ end
+ if line == "# OPTIONS" then
+ section = "options"
+ continue
+ end
+
+ if section == "name" and name == null then
+ name = line.trim
+ end
+ if section == "synopsis" and synopsis == null then
+ synopsis = line.trim
+ end
+ if section == "options" and line.has_prefix("###") then
+ var opts = new Array[String]
+ for opt in line.substring(3, line.length).trim.replace("`", "").split(",") do
+ opts.add opt.trim
+ end
+ var desc = ""
+ if i < lines.length - 1 then
+ desc = lines[i + 1].trim
+ end
+ options[opts] = desc
+ end
+ end
+ end
+
+ fun diff(toolcontext: ToolContext, ref: ManPage) do
+ if name != ref.name then
+ toolcontext.warning(mmodule.location, "diff-man",
+ "Warning: outdated man description. " +
+ "Expected `{ref.name or else ""}` got `{name or else ""}`.")
+ end
+ if synopsis != ref.synopsis then
+ toolcontext.warning(mmodule.location, "diff-man",
+ "Warning: outdated man synopsis. " +
+ "Expected `{ref.synopsis or else ""}` got `{synopsis or else ""}`.")
+ end
+ for name, desc in options do
+ if not ref.options.has_key(name) then
+ toolcontext.warning(mmodule.location, "diff-man",
+ "Warning: unknown man option `{name}`.`")
+ continue
+ end
+ var ref_desc = ref.options[name]
+ if desc != ref_desc then
+ toolcontext.warning(mmodule.location, "diff-man",
+ "Warning: outdated man option description. Expected `{ref_desc}` got `{desc}`.")
+ end
+ end
+ for ref_name, ref_desc in ref.options do
+ if not options.has_key(ref_name) then
+ toolcontext.warning(mmodule.location, "diff-man",
+ "Warning: missing man option `{ref_name}`.`")
+ end
+ end
+ end
+
+ redef fun to_s do
+ var tpl = new Template
+ tpl.addn "# NAME"
+ tpl.addn name or else ""
+ tpl.addn "# SYNOPSIS"
+ tpl.addn synopsis or else ""
+ tpl.addn "# OPTIONS"
+ for name, desc in options do
+ tpl.addn " * {name}: {desc}"
+ end
+ return tpl.write_to_string
+ end
+end
+
# build toolcontext
var toolcontext = new ToolContext
var tpl = new Template