nitc :: nitpackage
nitc :: nitpackage $ MModule
A Nit module is usually associated with a Nit source file.nitc :: nitpackage $ MModule
A Nit module is usually associated with a Nit source file.nitc :: actors_injection_phase
Injects model for the classes annotated with "is actor" sonitc :: astbuilder
Instantiation and transformation of semantic nodes in the AST of expressions and statementsnitc :: i18n_phase
Basic support of internationalization through the generation of id-to-string tablesSerializable::inspect
to show more useful information
nitc :: modelbuilder
more_collections :: more_collections
Highly specific, but useful, collections-related classes.threaded
annotation
serialization :: serialization_core
Abstract services to serialize Nit objects to different formatsnitc :: serialization_model_phase
Phase generating methods (model-only) to serialize Nit objectsnitc :: toolcontext
Common command-line tool infrastructure than handle options and error messagescore :: union_find
union–find algorithm using an efficient disjoint-set data structure
# Helpful features about packages
module nitpackage
import frontend
import doc::commands::commands_main
redef class ToolContext
# nitpackage phase
var nitpackage_phase: Phase = new NitPackagePhase(self, null)
# --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")
# --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")
# --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")
# --check-readme
var opt_check_readme = new OptionBool("Check README.md files", "--check-readme")
redef init do
super
option_context.add_option(opt_expand, opt_force)
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)
option_context.add_option(opt_check_readme)
end
end
private class NitPackagePhase
super Phase
redef fun process_mainmodule(mainmodule, mmodules) do
var mpackages = extract_mpackages(mmodules)
for mpackage in mpackages do
# Fictive and buggy packages are ignored
if not mpackage.has_source then
toolcontext.warning(mpackage.location, "no-source",
"Warning: `{mpackage}` has no source file")
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
# Check README.md
if toolcontext.opt_check_readme.value then
mpackage.check_readme(toolcontext)
continue
end
# Expand packages
if toolcontext.opt_expand.value and not mpackage.is_expanded then
var path = mpackage.expand
toolcontext.info("{mpackage} moved to {path}", 0)
end
if not mpackage.is_expanded then
toolcontext.warning(mpackage.location, "no-dir",
"Warning: `{mpackage}` has no package directory")
continue
end
# Create INI file
if toolcontext.opt_gen_ini.value then
if not mpackage.has_ini or toolcontext.opt_force.value then
var path = mpackage.gen_ini
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
# Extract the list of packages from the mmodules passed as arguments
fun extract_mpackages(mmodules: Collection[MModule]): Collection[MPackage] do
var mpackages = new ArraySet[MPackage]
for mmodule in mmodules do
var mpackage = mmodule.mpackage
if mpackage == null then continue
mpackages.add mpackage
end
return mpackages.to_a
end
end
redef class MPackage
# Expand `self` in its own directory
private fun expand: String do
assert not is_expanded
var ori_path = package_path.as(not null)
var new_path = ori_path.dirname / name
new_path.mkdir
sys.system "mv {ori_path} {new_path / name}.nit"
var ini_file = "{new_path}.ini"
if ini_file.file_exists then
sys.system "mv {new_path}.ini {new_path}/package.ini"
end
return new_path
end
private var maintainer: nullable String is lazy do
return git_exec("git shortlog -esn . | head -n 1 | sed 's/\\s*[0-9]*\\s*//'")
end
private var contributors: Array[String] is lazy do
var contribs = git_exec("git shortlog -esn . | head -n -1 | " +
"sed 's/\\s*[0-9]*\\s*//'")
if contribs == null then return new Array[String]
return contribs.split("\n")
end
private var git_url: nullable String is lazy do
var git = git_exec("git remote get-url origin")
if git == null then return null
git = git.replace("git@github.com:", "https://github.com/")
git = git.replace("git@gitlab.com:", "https://gitlab.com/")
return git
end
private var git_dir: nullable String is lazy do
return git_exec("git rev-parse --show-prefix")
end
private var browse_url: nullable String is lazy do
var git = git_url
if git == null then return null
var browse = git.replace(".git", "")
var dir = git_dir
if dir == null or dir.is_empty then return browse
return "{browse}/tree/master/{dir}"
end
private var homepage_url: nullable String is lazy do
var git = git_url
if git == null then return null
# Special case for nit files
if git.has_suffix("/nit.git") then
return "http://nitlanguage.org"
end
return git.replace(".git", "")
end
private var issues_url: nullable String is lazy do
var git = git_url
if git == null then return null
return "{git.replace(".git", "")}/issues"
end
private var license: nullable String is lazy do
var git = git_url
if git == null then return null
# Special case for nit files
if git.has_suffix("/nit.git") then
return "Apache-2.0"
end
return null
end
private fun git_exec(cmd: String): nullable String do
var path = package_path
if path == null then return null
if not is_expanded then path = path.dirname
with pr = new ProcessReader("sh", "-c", "cd {path} && {cmd}") do
return pr.read_all.trim
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 IniFile.from_file(ini_path)
ini.check_key(ini_path, toolcontext, self, "package.name", name)
ini.check_key(ini_path, toolcontext, self, "package.desc")
ini.check_key(ini_path, toolcontext, self, "package.tags")
# FIXME since `git reflog --follow` seems bugged
ini.check_key(ini_path, 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(ini_path, toolcontext, self, "package.license", license)
ini.check_key(ini_path, toolcontext, self, "upstream.browse", browse_url)
ini.check_key(ini_path, toolcontext, self, "upstream.git", git_url)
ini.check_key(ini_path, toolcontext, self, "upstream.git.directory", git_dir)
ini.check_key(ini_path, toolcontext, self, "upstream.homepage", homepage_url)
ini.check_key(ini_path, toolcontext, self, "upstream.issues", issues_url)
for key in ini.flatten.keys do
if not allowed_ini_keys.has(key) then
toolcontext.warning(location, "unknown-ini-key",
"Warning: ignoring unknown `{key}` key in `{ini_path}`")
end
end
end
private fun gen_ini: String do
var ini_path = self.ini_path.as(not null)
var ini = new IniFile.from_file(ini_path)
ini.update_value("package.name", name)
ini.update_value("package.desc", "")
ini.update_value("package.tags", "")
ini.update_value("package.maintainer", maintainer)
ini.update_value("package.more_contributors", contributors.join(","))
ini.update_value("package.license", license or else "")
ini.update_value("upstream.browse", browse_url)
ini.update_value("upstream.git", git_url)
ini.update_value("upstream.git.directory", git_dir)
ini.update_value("upstream.homepage", homepage_url)
ini.update_value("upstream.issues", issues_url)
ini.write_to_file(ini_path)
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 cmd_bin = new CmdMains(model, filter, 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 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(model, filter, 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 cmd = new CmdMains(model, filter, 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 cmd = new CmdMains(model, filter, 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
# README
private fun check_readme(toolcontext: ToolContext) do
if not has_readme then
toolcontext.error(location, "No `README.md` file for `{name}`")
return
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 IniFile
private fun check_key(ini_file: String, 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
self[key] = value
else
var old_value = self[key]
if not value.is_empty and old_value != value then
self[key] = value
end
end
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
tpl.add "Usage: nitpackage [OPTION]... <file.nit>...\n"
tpl.add "Helpful features about packages."
toolcontext.tooldescription = tpl.write_to_string
# process options
toolcontext.process_options(args)
var arguments = toolcontext.option_context.rest
# build model
var model = new Model
var mbuilder = new ModelBuilder(model, toolcontext)
var mmodules = mbuilder.parse_full(arguments)
# process
if mmodules.is_empty then return
mbuilder.run_phases
toolcontext.run_global_phases(mmodules)
src/nitpackage.nit:15,1--791,39