nitpackage: add option to check existing package.ini 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
20 redef class ToolContext
21 # --expand
22 var opt_expand = new OptionBool("Move singleton packages to their own directory", "--expand")
23
24 # --check-ini
25 var opt_check_ini = new OptionBool("Check package.ini files", "--check-ini")
26
27 # --gen-ini
28 var opt_gen_ini = new OptionBool("Generate package.ini files", "--gen-ini")
29
30 # --force
31 var opt_force = new OptionBool("Force update of existing files", "-f", "--force")
32
33 # README handling phase
34 var readme_phase: Phase = new ReadmePhase(self, null)
35
36 redef init do
37 super
38 option_context.add_option(opt_expand, opt_force)
39 option_context.add_option(opt_check_ini, opt_gen_ini)
40 end
41 end
42
43 private class ReadmePhase
44 super Phase
45
46 redef fun process_mainmodule(mainmodule, mmodules) do
47 var mpackages = extract_mpackages(mmodules)
48 for mpackage in mpackages do
49
50 # Fictive and buggy packages are ignored
51 if not mpackage.has_source then
52 toolcontext.warning(mpackage.location, "no-source",
53 "Warning: `{mpackage}` has no source file")
54 continue
55 end
56
57 # Check package INI files
58 if toolcontext.opt_check_ini.value then
59 mpackage.check_ini(toolcontext)
60 continue
61 end
62
63 # Expand packages
64 if toolcontext.opt_expand.value and not mpackage.is_expanded then
65 var path = mpackage.expand
66 toolcontext.info("{mpackage} moved to {path}", 0)
67 end
68 if not mpackage.is_expanded then
69 toolcontext.warning(mpackage.location, "no-dir",
70 "Warning: `{mpackage}` has no package directory")
71 continue
72 end
73
74 # Create INI file
75 if toolcontext.opt_gen_ini.value then
76 if not mpackage.has_ini or toolcontext.opt_force.value then
77 var path = mpackage.gen_ini
78 toolcontext.info("generated INI file `{path}`", 0)
79 end
80 end
81 end
82 end
83
84 # Extract the list of packages from the mmodules passed as arguments
85 fun extract_mpackages(mmodules: Collection[MModule]): Collection[MPackage] do
86 var mpackages = new ArraySet[MPackage]
87 for mmodule in mmodules do
88 var mpackage = mmodule.mpackage
89 if mpackage == null then continue
90 mpackages.add mpackage
91 end
92 return mpackages.to_a
93 end
94 end
95
96 redef class MPackage
97
98 # Expand `self` in its own directory
99 private fun expand: String do
100 assert not is_expanded
101
102 var ori_path = package_path.as(not null)
103 var new_path = ori_path.dirname / name
104
105 new_path.mkdir
106 sys.system "mv {ori_path} {new_path / name}.nit"
107
108 var ini_file = "{new_path}.ini"
109 if ini_file.file_exists then
110 sys.system "mv {new_path}.ini {new_path}/package.ini"
111 end
112
113 return new_path
114 end
115
116 private var maintainer: nullable String is lazy do
117 return git_exec("git shortlog -esn . | head -n 1 | sed 's/\\s*[0-9]*\\s*//'")
118 end
119
120 private var contributors: Array[String] is lazy do
121 var contribs = git_exec("git shortlog -esn . | head -n -1 | " +
122 "sed 's/\\s*[0-9]*\\s*//'")
123 if contribs == null then return new Array[String]
124 return contribs.split("\n")
125 end
126
127 private var git_url: nullable String is lazy do
128 var git = git_exec("git remote get-url origin")
129 if git == null then return null
130 git = git.replace("git@github.com:", "https://github.com/")
131 git = git.replace("git@gitlab.com:", "https://gitlab.com/")
132 return git
133 end
134
135 private var git_dir: nullable String is lazy do
136 return git_exec("git rev-parse --show-prefix")
137 end
138
139 private var browse_url: nullable String is lazy do
140 var git = git_url
141 if git == null then return null
142 var browse = git.replace(".git", "")
143 var dir = git_dir
144 if dir == null or dir.is_empty then return browse
145 return "{browse}/tree/master/{dir}"
146 end
147
148 private var homepage_url: nullable String is lazy do
149 var git = git_url
150 if git == null then return null
151 # Special case for nit files
152 if git.has_suffix("/nit.git") then
153 return "http://nitlanguage.org"
154 end
155 return git.replace(".git", "")
156 end
157
158 private var issues_url: nullable String is lazy do
159 var git = git_url
160 if git == null then return null
161 return "{git.replace(".git", "")}/issues"
162 end
163
164 private var license: nullable String is lazy do
165 var git = git_url
166 if git == null then return null
167 # Special case for nit files
168 if git.has_suffix("/nit.git") then
169 return "Apache-2.0"
170 end
171 return null
172 end
173
174 private fun git_exec(cmd: String): nullable String do
175 var path = package_path
176 if path == null then return null
177 if not is_expanded then path = path.dirname
178 with pr = new ProcessReader("sh", "-c", "cd {path} && {cmd}") do
179 return pr.read_all.trim
180 end
181 end
182
183 private var allowed_ini_keys = [
184 "package.name", "package.desc", "package.tags", "package.license",
185 "package.maintainer", "package.more_contributors",
186 "upstream.browse", "upstream.git", "upstream.git.directory",
187 "upstream.homepage", "upstream.issues"
188 ]
189
190 private fun check_ini(toolcontext: ToolContext) do
191 if not has_ini then
192 toolcontext.error(location, "No `package.ini` file for `{name}`")
193 return
194 end
195
196 var pkg_path = package_path
197 if pkg_path == null then return
198
199 var ini_path = ini_path
200 if ini_path == null then return
201
202 var ini = new ConfigTree(ini_path)
203
204 ini.check_key(toolcontext, self, "package.name", name)
205 ini.check_key(toolcontext, self, "package.desc")
206 ini.check_key(toolcontext, self, "package.tags")
207
208 # FIXME since `git reflog --follow` seems bugged
209 ini.check_key(toolcontext, self, "package.maintainer")
210 # var maint = mpackage.maintainer
211 # if maint != null then
212 # ini.check_key(toolcontext, self, "package.maintainer", maint)
213 # end
214
215 # FIXME since `git reflog --follow` seems bugged
216 # var contribs = mpackage.contributors
217 # if contribs.not_empty then
218 # ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
219 # end
220
221 ini.check_key(toolcontext, self, "package.license", license)
222 ini.check_key(toolcontext, self, "upstream.browse", browse_url)
223 ini.check_key(toolcontext, self, "upstream.git", git_url)
224 ini.check_key(toolcontext, self, "upstream.git.directory", git_dir)
225 ini.check_key(toolcontext, self, "upstream.homepage", homepage_url)
226 ini.check_key(toolcontext, self, "upstream.issues", issues_url)
227
228 for key in ini.to_map.keys do
229 if not allowed_ini_keys.has(key) then
230 toolcontext.warning(location, "unknown-ini-key",
231 "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
232 end
233 end
234 end
235
236 private fun gen_ini: String do
237 var ini_path = self.ini_path.as(not null)
238 var ini = new ConfigTree(ini_path)
239
240 ini.update_value("package.name", name)
241 ini.update_value("package.desc", "")
242 ini.update_value("package.tags", "")
243 ini.update_value("package.maintainer", maintainer)
244 ini.update_value("package.more_contributors", contributors.join(","))
245 ini.update_value("package.license", license or else "")
246
247 ini.update_value("upstream.browse", browse_url)
248 ini.update_value("upstream.git", git_url)
249 ini.update_value("upstream.git.directory", git_dir)
250 ini.update_value("upstream.homepage", homepage_url)
251 ini.update_value("upstream.issues", issues_url)
252
253 ini.save
254 return ini_path
255 end
256 end
257
258 redef class ConfigTree
259 private fun check_key(toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
260 if not has_key(key) then
261 toolcontext.warning(mpackage.location, "missing-ini-key",
262 "Warning: missing `{key}` key in `{ini_file}`")
263 return
264 end
265 if self[key].as(not null).is_empty then
266 toolcontext.warning(mpackage.location, "missing-ini-value",
267 "Warning: empty `{key}` key in `{ini_file}`")
268 return
269 end
270 if value != null and self[key] != value then
271 toolcontext.warning(mpackage.location, "wrong-ini-value",
272 "Warning: wrong value for `{key}` in `{ini_file}`. " +
273 "Expected `{value}`, got `{self[key] or else ""}`")
274 end
275 end
276
277 private fun update_value(key: String, value: nullable String) do
278 if value == null then return
279 if not has_key(key) then
280 self[key] = value
281 else
282 var old_value = self[key]
283 if not value.is_empty and old_value != value then
284 self[key] = value
285 end
286 end
287 end
288 end
289
290 # build toolcontext
291 var toolcontext = new ToolContext
292 var tpl = new Template
293 tpl.add "Usage: nitpackage [OPTION]... <file.nit>...\n"
294 tpl.add "Helpful features about packages."
295 toolcontext.tooldescription = tpl.write_to_string
296
297 # process options
298 toolcontext.process_options(args)
299 var arguments = toolcontext.option_context.rest
300
301 # build model
302 var model = new Model
303 var mbuilder = new ModelBuilder(model, toolcontext)
304 var mmodules = mbuilder.parse_full(arguments)
305
306 # process
307 if mmodules.is_empty then return
308 mbuilder.run_phases
309 toolcontext.run_global_phases(mmodules)