c_src: update Makefile to compile on MacOSX
[nit.git] / src / android_annotations.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Annotations to gather metadata on Android projects. Get the metadata
18 # by calling `ModelBuilder::android_project_for`.
19 module android_annotations
20
21 import parser_util
22 import modelbuilder
23 import modelize_property
24 import literal
25 import typing
26
27 # Metadata associated to an Android project
28 class AndroidProject
29 # Name of the resulting application
30 var name: nullable String = null
31
32 # Java package used to identify the APK
33 var java_package: nullable String = null
34
35 # Version of the Android application and APK
36 var version: nullable String = null
37
38 # Numerical version code of the Android application and APK
39 var version_code: Int = 0
40
41 # Custom lines to add to the AndroidManifest.xml in the <manifest> node
42 var manifest_lines = new Array[String]
43
44 # Custom lines to add to the AndroidManifest.xml in the <application> node
45 var manifest_application_lines = new Array[String]
46
47 redef fun to_s do return """
48 name: {{{name or else "null"}}}
49 namespace: {{{java_package or else "null"}}}
50 version: {{{version or else "null"}}}"""
51 end
52
53 redef class ModelBuilder
54 # Get the `AndroidProject` gathered from `mmodule` and its importations
55 fun android_project_for(mmodule: MModule): AndroidProject
56 do
57 var project = new AndroidProject
58
59 var annot = priority_annotation_on_modules("app_name", mmodule)
60 if annot != null then project.name = annot.arg_as_string(self)
61
62 annot = priority_annotation_on_modules("app_version", mmodule)
63 if annot != null then project.version = annot.as_version(self)
64
65 annot = priority_annotation_on_modules("java_package", mmodule)
66 if annot != null then project.java_package = annot.arg_as_string(self)
67
68 var annots = collect_annotations_on_modules("android_manifest", mmodule)
69 for an in annots do project.manifest_lines.add an.arg_as_string(self)
70
71 annots = collect_annotations_on_modules("android_manifest_application", mmodule)
72 for an in annots do project.manifest_application_lines.add an.arg_as_string(self)
73
74 # Get the date and time (down to the minute) as string
75 var local_time = new Tm.localtime
76 var local_time_s = local_time.strftime("%y%m%d%H%M")
77 project.version_code = local_time_s.to_i
78
79 toolcontext.check_errors
80
81 return project
82 end
83
84 # Recursively collect all annotations by name in `mmodule` and its importations (direct and indirect)
85 private fun collect_annotations_on_modules(name: String, mmodule: MModule): Array[AAnnotation]
86 do
87 var annotations = new Array[AAnnotation]
88 for mmod in mmodule.in_importation.greaters do
89 if not mmodule2nmodule.keys.has(mmod) then continue
90 var amod = mmodule2nmodule[mmod]
91 var module_decl = amod.n_moduledecl
92 if module_decl == null then continue
93 var aas = module_decl.collect_annotations_by_name(name)
94 annotations.add_all aas
95 end
96 return annotations
97 end
98
99 # Get an annotation by name from `mmodule` and its super modules. Will recursively search
100 # in imported module to find the "latest" declaration and detects priority conflicts.
101 private fun priority_annotation_on_modules(name: String, mmodule: MModule): nullable AAnnotation
102 do
103 if mmodule2nmodule.keys.has(mmodule) then
104 var amod = mmodule2nmodule[mmodule]
105 var module_decl = amod.n_moduledecl
106 if module_decl != null then
107 var annotations = module_decl.collect_annotations_by_name(name)
108 if annotations.length == 1 then
109 return annotations.first
110 else if annotations.length > 1 then
111 toolcontext.error(mmodule.location,
112 "Multiple declaration of annotation {name}, it must be defined only once.")
113 end
114 end
115 end
116
117 var sources = new Array[MModule]
118 var annotations = null
119 for mmod in mmodule.in_importation.direct_greaters do
120 var res = priority_annotation_on_modules(name, mmod)
121 if res != null then
122 sources.add mmod
123 annotations = res
124 end
125 end
126 if sources.length > 1 then
127 toolcontext.error(mmodule.location,
128 "Priority conflict on annotation {name}, it has been defined in: {sources.join(", ")}")
129 return null
130 end
131 return annotations
132 end
133 end
134
135 redef class AAnnotation
136 # Get the single argument of `self` as a `String`. Raise error on any inconsistency.
137 private fun arg_as_string(modelbuilder: ModelBuilder): String
138 do
139 var annotation_name = n_atid.n_id.text
140 var format_error = "Annotation error: \"{annotation_name}\" expects a single String as argument."
141
142 var args = n_args
143 var platform_name
144 if args.length != 1 then
145 modelbuilder.error(self, format_error)
146 return ""
147 else
148 var arg = args.first
149
150 if not arg isa AExprAtArg then
151 modelbuilder.error(self, format_error)
152 return ""
153 end
154
155 var expr = arg.n_expr
156 if not expr isa AStringFormExpr then
157 modelbuilder.error(self, format_error)
158 return ""
159 end
160 return expr.value.as(not null)
161 end
162 end
163
164 # Returns a version string (example: "1.5.6b42a7c") from an annotation `version(1, 5, git_revision)`.
165 #
166 # The user can enter as many fields as needed. The call to `git_revision` will be replaced by the short
167 # revision number. If the working tree is dirty, it will append another field with "d" for dirty.
168 private fun as_version(modelbuilder: ModelBuilder): String
169 do
170 var annotation_name = n_atid.n_id.text
171 var version_fields = new Array[Object]
172
173 var args = n_args
174 var platform_name
175 if args.length < 1 then
176 modelbuilder.error(self, "Annotation error: \"{annotation_name}\" expects at least a single argument.")
177 return ""
178 else
179 for arg in args do
180 var format_error = "Annotation error: \"{annotation_name}\" expects its arguments to be of type Int or a call to `git_revision`"
181
182 if not arg isa AExprAtArg then
183 modelbuilder.error(self, format_error)
184 return ""
185 end
186
187 var expr = arg.n_expr
188 if expr isa AIntExpr then
189 var value = expr.value
190 assert value != null
191 version_fields.add value
192 else if expr isa AStringFormExpr then
193 version_fields.add expr.value.as(not null)
194 else if expr isa ACallExpr then
195 # We support calls to "git" only
196 var exec_args = expr.n_args.to_a
197 if expr.n_id.text != "git_revision" or not exec_args.is_empty then
198 modelbuilder.error(self,
199 "Annotation error: \"{annotation_name}\" accepts only calls to `git_revision` with the command as arguments.")
200 return ""
201 end
202
203 # Get Git short revision
204 var proc = new IProcess("git", "rev-parse", "--short", "HEAD")
205 proc.wait
206 assert proc.status == 0
207 var lines = proc.read_all
208 var revision = lines.split("\n").first
209
210 # Is it dirty?
211 # If not, the return of `git diff --shortstat` is an empty line
212 proc = new IProcess("git", "diff-index", "--quiet", "HEAD")
213 proc.wait
214 var dirty = proc.status != 0
215 if dirty then revision += ".d"
216
217 version_fields.add revision
218 else
219 modelbuilder.error(self, format_error)
220 return ""
221 end
222 end
223 end
224
225 return version_fields.join(".")
226 end
227 end