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
23 var opt_expand
= new OptionBool("Move singleton packages to their own directory", "--expand")
26 var opt_check_ini
= new OptionBool("Check package.ini files", "--check-ini")
29 var opt_gen_ini
= new OptionBool("Generate package.ini files", "--gen-ini")
32 var opt_force
= new OptionBool("Force update of existing files", "-f", "--force")
35 var opt_check_makefile
= new OptionBool("Check Makefile files", "--check-makefile")
38 var opt_gen_makefile
= new OptionBool("Generate Makefile files", "--gen-makefile")
41 var nitpackage_phase
: Phase = new NitPackagePhase(self, null)
45 option_context
.add_option
(opt_expand
, opt_force
)
46 option_context
.add_option
(opt_check_ini
, opt_gen_ini
)
47 option_context
.add_option
(opt_check_makefile
, opt_gen_makefile
)
51 private class NitPackagePhase
54 redef fun process_mainmodule
(mainmodule
, mmodules
) do
55 var mpackages
= extract_mpackages
(mmodules
)
56 for mpackage
in mpackages
do
58 # Fictive and buggy packages are ignored
59 if not mpackage
.has_source
then
60 toolcontext
.warning
(mpackage
.location
, "no-source",
61 "Warning: `{mpackage}` has no source file")
65 # Check package INI files
66 if toolcontext
.opt_check_ini
.value
then
67 mpackage
.check_ini
(toolcontext
)
71 # Check package Makefiles
72 if toolcontext
.opt_check_makefile
.value
then
73 mpackage
.check_makefile
(toolcontext
, mainmodule
)
78 if toolcontext
.opt_expand
.value
and not mpackage
.is_expanded
then
79 var path
= mpackage
.expand
80 toolcontext
.info
("{mpackage} moved to {path}", 0)
82 if not mpackage
.is_expanded
then
83 toolcontext
.warning
(mpackage
.location
, "no-dir",
84 "Warning: `{mpackage}` has no package directory")
89 if toolcontext
.opt_gen_ini
.value
then
90 if not mpackage
.has_ini
or toolcontext
.opt_force
.value
then
91 var path
= mpackage
.gen_ini
92 toolcontext
.info
("generated INI file `{path}`", 0)
97 if toolcontext
.opt_gen_makefile
.value
then
98 if not mpackage
.has_makefile
or toolcontext
.opt_force
.value
then
99 var path
= mpackage
.gen_makefile
(toolcontext
.modelbuilder
.model
, mainmodule
)
101 toolcontext
.info
("generated Makefile `{path}`", 0)
108 # Extract the list of packages from the mmodules passed as arguments
109 fun extract_mpackages
(mmodules
: Collection[MModule]): Collection[MPackage] do
110 var mpackages
= new ArraySet[MPackage]
111 for mmodule
in mmodules
do
112 var mpackage
= mmodule
.mpackage
113 if mpackage
== null then continue
114 mpackages
.add mpackage
116 return mpackages
.to_a
122 # Expand `self` in its own directory
123 private fun expand
: String do
124 assert not is_expanded
126 var ori_path
= package_path
.as(not null)
127 var new_path
= ori_path
.dirname
/ name
130 sys
.system
"mv {ori_path} {new_path / name}.nit"
132 var ini_file
= "{new_path}.ini"
133 if ini_file
.file_exists
then
134 sys
.system
"mv {new_path}.ini {new_path}/package.ini"
140 private var maintainer
: nullable String is lazy
do
141 return git_exec
("git shortlog -esn . | head -n 1 | sed 's/\\s*[0-9]*\\s*//'")
144 private var contributors
: Array[String] is lazy
do
145 var contribs
= git_exec
("git shortlog -esn . | head -n -1 | " +
146 "sed 's/\\s*[0-9]*\\s*//'")
147 if contribs
== null then return new Array[String]
148 return contribs
.split
("\n")
151 private var git_url
: nullable String is lazy
do
152 var git
= git_exec
("git remote get-url origin")
153 if git
== null then return null
154 git
= git
.replace
("git@github.com:", "https://github.com/")
155 git
= git
.replace
("git@gitlab.com:", "https://gitlab.com/")
159 private var git_dir
: nullable String is lazy
do
160 return git_exec
("git rev-parse --show-prefix")
163 private var browse_url
: nullable String is lazy
do
165 if git
== null then return null
166 var browse
= git
.replace
(".git", "")
168 if dir
== null or dir
.is_empty
then return browse
169 return "{browse}/tree/master/{dir}"
172 private var homepage_url
: nullable String is lazy
do
174 if git
== null then return null
175 # Special case for nit files
176 if git
.has_suffix
("/nit.git") then
177 return "http://nitlanguage.org"
179 return git
.replace
(".git", "")
182 private var issues_url
: nullable String is lazy
do
184 if git
== null then return null
185 return "{git.replace(".git", "")}/issues"
188 private var license
: nullable String is lazy
do
190 if git
== null then return null
191 # Special case for nit files
192 if git
.has_suffix
("/nit.git") then
198 private fun git_exec
(cmd
: String): nullable String do
199 var path
= package_path
200 if path
== null then return null
201 if not is_expanded
then path
= path
.dirname
202 with pr
= new ProcessReader("sh", "-c", "cd {path} && {cmd}") do
203 return pr
.read_all
.trim
207 private var allowed_ini_keys
= [
208 "package.name", "package.desc", "package.tags", "package.license",
209 "package.maintainer", "package.more_contributors",
210 "upstream.browse", "upstream.git", "upstream.git.directory",
211 "upstream.homepage", "upstream.issues"
214 private fun check_ini
(toolcontext
: ToolContext) do
216 toolcontext
.error
(location
, "No `package.ini` file for `{name}`")
220 var pkg_path
= package_path
221 if pkg_path
== null then return
223 var ini_path
= ini_path
224 if ini_path
== null then return
226 var ini
= new ConfigTree(ini_path
)
228 ini
.check_key
(toolcontext
, self, "package.name", name
)
229 ini
.check_key
(toolcontext
, self, "package.desc")
230 ini
.check_key
(toolcontext
, self, "package.tags")
232 # FIXME since `git reflog --follow` seems bugged
233 ini
.check_key
(toolcontext
, self, "package.maintainer")
234 # var maint = mpackage.maintainer
235 # if maint != null then
236 # ini.check_key(toolcontext, self, "package.maintainer", maint)
239 # FIXME since `git reflog --follow` seems bugged
240 # var contribs = mpackage.contributors
241 # if contribs.not_empty then
242 # ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
245 ini
.check_key
(toolcontext
, self, "package.license", license
)
246 ini
.check_key
(toolcontext
, self, "upstream.browse", browse_url
)
247 ini
.check_key
(toolcontext
, self, "upstream.git", git_url
)
248 ini
.check_key
(toolcontext
, self, "upstream.git.directory", git_dir
)
249 ini
.check_key
(toolcontext
, self, "upstream.homepage", homepage_url
)
250 ini
.check_key
(toolcontext
, self, "upstream.issues", issues_url
)
252 for key
in ini
.to_map
.keys
do
253 if not allowed_ini_keys
.has
(key
) then
254 toolcontext
.warning
(location
, "unknown-ini-key",
255 "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
260 private fun gen_ini
: String do
261 var ini_path
= self.ini_path
.as(not null)
262 var ini
= new ConfigTree(ini_path
)
264 ini
.update_value
("package.name", name
)
265 ini
.update_value
("package.desc", "")
266 ini
.update_value
("package.tags", "")
267 ini
.update_value
("package.maintainer", maintainer
)
268 ini
.update_value
("package.more_contributors", contributors
.join
(","))
269 ini
.update_value
("package.license", license
or else "")
271 ini
.update_value
("upstream.browse", browse_url
)
272 ini
.update_value
("upstream.git", git_url
)
273 ini
.update_value
("upstream.git.directory", git_dir
)
274 ini
.update_value
("upstream.homepage", homepage_url
)
275 ini
.update_value
("upstream.issues", issues_url
)
283 # The path to `self` Makefile
284 fun makefile_path
: nullable String do
285 var path
= package_path
286 if path
== null then return null
287 if not is_expanded
then return null
288 return path
/ "Makefile"
291 # Does `self` have a Makefile?
292 fun has_makefile
: Bool do
293 var makefile_path
= self.makefile_path
294 if makefile_path
== null then return false
295 return makefile_path
.file_exists
298 private fun check_makefile
(toolcontext
: ToolContext, mainmodule
: MModule) do
299 var model
= toolcontext
.modelbuilder
.model
300 var filter
= new ModelFilter(accept_example
= false, accept_test
= false)
301 var view
= new ModelView(model
, mainmodule
, filter
)
303 var cmd_bin
= new CmdMains(view
, mentity
= self)
304 var res_bin
= cmd_bin
.init_command
305 if not res_bin
isa CmdSuccess then return
307 for mmodule
in cmd_bin
.results
.as(not null) do
308 if not mmodule
isa MModule then continue
310 if mmodule
.makefile_path
== null then
311 toolcontext
.warning
(location
, "missing-makefile",
312 "Warning: no Makefile for executable module `{mmodule.full_name}`")
317 private fun gen_makefile
(model
: Model, mainmodule
: MModule): nullable String do
318 var filter
= new ModelFilter(accept_example
= false, accept_test
= false)
319 var view
= new ModelView(model
, mainmodule
, filter
)
321 var pkg_path
= package_path
.as(not null)
322 var makefile_path
= makefile_path
.as(not null)
324 var bins
= new Array[String]
325 var cmd_bin
= new CmdMains(view
, mentity
= self)
326 var res_bin
= cmd_bin
.init_command
327 if res_bin
isa CmdSuccess then
328 for mmodule
in cmd_bin
.results
.as(not null) do
329 if not mmodule
isa MModule then continue
330 var mmodule_makefile
= mmodule
.makefile_path
331 if mmodule_makefile
!= null and mmodule_makefile
!= makefile_path
then continue
333 var file
= mmodule
.location
.file
334 if file
== null then continue
335 # Remove package path prefix
336 var bin_path
= file
.filename
337 if pkg_path
.has_suffix
("/") then
338 bin_path
= bin_path
.replace
(pkg_path
, "")
340 bin_path
= bin_path
.replace
("{pkg_path}/", "")
346 if bins
.is_empty
then return null
348 var make
= new NitMakefile(bins
)
349 make
.render
.write_to_file
(makefile_path
)
355 private fun makefile_path
: nullable String do
356 var file
= location
.file
357 if file
== null then return null
359 var dir
= file
.filename
.dirname
360 var makefile
= (dir
/ "Makefile")
361 if not makefile
.file_exists
then return null
363 for line
in makefile
.to_path
.read_lines
do
364 if line
.has_prefix
("{name}:") then return makefile
370 redef class ConfigTree
371 private fun check_key
(toolcontext
: ToolContext, mpackage
: MPackage, key
: String, value
: nullable String) do
372 if not has_key
(key
) then
373 toolcontext
.warning
(mpackage
.location
, "missing-ini-key",
374 "Warning: missing `{key}` key in `{ini_file}`")
377 if self[key
].as(not null).is_empty
then
378 toolcontext
.warning
(mpackage
.location
, "missing-ini-value",
379 "Warning: empty `{key}` key in `{ini_file}`")
382 if value
!= null and self[key
] != value
then
383 toolcontext
.warning
(mpackage
.location
, "wrong-ini-value",
384 "Warning: wrong value for `{key}` in `{ini_file}`. " +
385 "Expected `{value}`, got `{self[key] or else ""}`")
389 private fun update_value
(key
: String, value
: nullable String) do
390 if value
== null then return
391 if not has_key
(key
) then
394 var old_value
= self[key
]
395 if not value
.is_empty
and old_value
!= value
then
402 # A Makefile for the Nit project
405 # Nit files to compile
406 var nit_files
: Array[String]
408 # List of rules to add in the Makefile
409 fun rules
: Array[MakeRule] do
410 var rules
= new Array[MakeRule]
412 var rule_all
= new MakeRule("all", is_phony
= true)
415 for file
in nit_files
do
416 var bin
= file
.basename
.strip_extension
418 rule_all
.deps
.add
"bin/{bin}"
420 var rule
= new MakeRule("bin/{bin}")
421 rule
.deps
.add
"$(shell $(NITLS) -M {file})"
422 rule
.lines
.add
"mkdir -p bin/"
423 rule
.lines
.add
"$(NITC) {file} -o bin/{bin}"
427 var rule_check
= new MakeRule("check", is_phony
= true)
428 rule_check
.lines
.add
"$(NITUNIT) ."
431 var rule_doc
= new MakeRule("doc", is_phony
= true)
432 rule_doc
.lines
.add
"$(NITDOC) . -o doc/"
435 var rule_clean
= new MakeRule("clean", is_phony
= true)
436 if nit_files
.not_empty
then
437 rule_clean
.lines
.add
"rm -rf bin/"
439 rule_clean
.lines
.add
"rm -rf doc/"
446 fun render
: Writable do
447 var tpl
= new Template
449 # This file is part of NIT ( http://www.nitlanguage.org ).
451 # Licensed under the Apache License, Version 2.0 (the "License");
452 # you may not use this file except in compliance with the License.
453 # You may obtain a copy of the License at
455 # http://www.apache.org/licenses/LICENSE-2.0
457 # Unless required by applicable law or agreed to in writing, software
458 # distributed under the License is distributed on an "AS IS" BASIS,
459 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
460 # See the License for the specific language governing permissions and
461 # limitations under the License.\n"""
463 if nit_files
.not_empty
then
464 tpl
.addn
"NITC ?= nitc"
465 tpl
.addn
"NITLS ?= nitls"
467 tpl
.addn
"NITUNIT ?= nitunit"
468 tpl
.addn
"NITDOC ?= nitdoc"
471 tpl
.add
"\n{rule.render.write_to_string}"
478 # A rule that goes into a Makefile
484 # Is this rule a `.PHONY` one?
485 var is_phony
: Bool = false is optional
488 var deps
= new Array[String]
491 var lines
= new Array[String]
494 fun render
: Writable do
495 var tpl
= new Template
497 tpl
.addn
".PHONY: {name}"
500 if deps
.not_empty
then
501 tpl
.add
" {deps.join(" ")}"
512 var toolcontext
= new ToolContext
513 var tpl
= new Template
514 tpl
.add
"Usage: nitpackage [OPTION]... <file.nit>...\n"
515 tpl
.add
"Helpful features about packages."
516 toolcontext
.tooldescription
= tpl
.write_to_string
519 toolcontext
.process_options
(args
)
520 var arguments
= toolcontext
.option_context
.rest
523 var model
= new Model
524 var mbuilder
= new ModelBuilder(model
, toolcontext
)
525 var mmodules
= mbuilder
.parse_full
(arguments
)
528 if mmodules
.is_empty
then return
530 toolcontext
.run_global_phases
(mmodules
)