Mercurial > hg > truffle
comparison mxtool/mx.py @ 3723:6c5f528c7aac
Added a copy of the mxtool to repo.
author | Doug Simon <doug.simon@oracle.com> |
---|---|
date | Fri, 16 Dec 2011 16:46:33 +0100 |
parents | |
children | 3e2e8b8abdaf |
comparison
equal
deleted
inserted
replaced
3722:7c5524a4e86e | 3723:6c5f528c7aac |
---|---|
1 #!/usr/bin/python | |
2 # | |
3 # ---------------------------------------------------------------------------------------------------- | |
4 # | |
5 # Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved. | |
6 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
7 # | |
8 # This code is free software; you can redistribute it and/or modify it | |
9 # under the terms of the GNU General Public License version 2 only, as | |
10 # published by the Free Software Foundation. | |
11 # | |
12 # This code is distributed in the hope that it will be useful, but WITHOUT | |
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
14 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
15 # version 2 for more details (a copy is included in the LICENSE file that | |
16 # accompanied this code). | |
17 # | |
18 # You should have received a copy of the GNU General Public License version | |
19 # 2 along with this work; if not, write to the Free Software Foundation, | |
20 # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
21 # | |
22 # Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
23 # or visit www.oracle.com if you need additional information or have any | |
24 # questions. | |
25 # | |
26 # ---------------------------------------------------------------------------------------------------- | |
27 # | |
28 # mx is a command line tool inspired by mvn (http://maven.apache.org/) | |
29 # and hg (http://mercurial.selenic.com/). It includes a mechanism for | |
30 # managing the dependencies between a set of projects (like Maven) | |
31 # as well as making it simple to run commands | |
32 # (like hg is the interface to the Mercurial commands). | |
33 # | |
34 # When launched, mx looks for a mx configuration (i.e. a directory named 'mx') | |
35 # in the current working directory. This is the primary mx configuration. Any | |
36 # other mx configurations are included mx configurations. | |
37 # | |
38 # If an mx configuration exists, then the following files in the configuration | |
39 # are processed (if they exist): | |
40 # | |
41 # projects - Lists projects, libraries and dependencies between them | |
42 # commands.py - Extensions to the commands launchable by mx. This is only processed | |
43 # for the primary mx configuration. | |
44 # includes - Other directories containing mx configurations to be loaded. | |
45 # This is a recursive action. | |
46 # env - A set of environment variable definitions. | |
47 # | |
48 # The MX_INCLUDES environment variable can also be used to specify | |
49 # other directories containing mx configurations. | |
50 # This value of this variable has the same format as a Java class path. | |
51 # | |
52 # The includes and env files are typically not put under version control | |
53 # as they usually contain local filesystem paths. | |
54 # | |
55 # The projects file is like the pom.xml file from Maven except that | |
56 # it is in a properties file format instead of XML. Each non-comment line | |
57 # in the file specifies an attribute of a project or library. The main | |
58 # difference between a project and a library is that the former contains | |
59 # source code built by the mx tool where as the latter is an external | |
60 # dependency. The format of the projects file is | |
61 # | |
62 # Library specification format: | |
63 # | |
64 # library@<name>@<prop>=<value> | |
65 # | |
66 # Built-in library properties (* = required): | |
67 # | |
68 # *path: the file system path for the library to appear on a class path | |
69 # urls: a comma seperated list of URLs from which the library can be downloaded | |
70 # optional: if "true" then this library will be omitted from a class path if it doesn't exist on the file system and no URLs are specified | |
71 # | |
72 # Project specification format: | |
73 # | |
74 # project@<name>@<prop>=<value> | |
75 # | |
76 # The name of a project also denotes the directory it is in. | |
77 # | |
78 # Built-in project properties: | |
79 # | |
80 # *sourceDirs: a comma separated list of source directoriy names (relative to the project directory) | |
81 # dependencies: a comma separated list of the libraries and project the project depends upon (transitive dependencies may be omitted) | |
82 # checkstyle: the project whose Checkstyle configuration (i.e. <project>/.checkstyle_checks.xml) is used | |
83 # | |
84 # Other properties can be specified for projects and libraries for use by extension commands. | |
85 # | |
86 # Values can use environment variables with Bash syntax (e.g. ${HOME}). | |
87 | |
88 import sys | |
89 import os | |
90 import subprocess | |
91 from collections import Callable | |
92 from threading import Thread | |
93 from argparse import ArgumentParser, REMAINDER | |
94 from os.path import join, dirname, exists, getmtime, isabs, expandvars, isdir | |
95 import shlex | |
96 import types | |
97 import urllib2 | |
98 import contextlib | |
99 import StringIO | |
100 import zipfile | |
101 import shutil, fnmatch, re, xml.dom.minidom | |
102 | |
103 DEFAULT_JAVA_ARGS = '-ea -Xss2m -Xmx1g' | |
104 | |
105 class Dependency: | |
106 def __init__(self, name, baseDir): | |
107 self.name = name | |
108 self.baseDir = baseDir | |
109 self.env = None | |
110 | |
111 def __str__(self): | |
112 return self.name | |
113 | |
114 def __eq__(self, other): | |
115 return self.name == other.name | |
116 | |
117 def __ne__(self, other): | |
118 return self.name != other.name | |
119 | |
120 def __hash__(self): | |
121 return hash(self.name) | |
122 | |
123 def isLibrary(self): | |
124 return isinstance(self, Library) | |
125 | |
126 class Project(Dependency): | |
127 def __init__(self, baseDir, name, srcDirs, deps): | |
128 Dependency.__init__(self, name, baseDir) | |
129 self.srcDirs = srcDirs | |
130 self.deps = deps | |
131 self.checkstyleProj = name | |
132 self.dir = join(baseDir, name) | |
133 self.native = False | |
134 | |
135 def all_deps(self, deps, pdb, includeLibs): | |
136 if self in deps: | |
137 return deps | |
138 for name in self.deps: | |
139 assert name != self.name | |
140 dep = pdb.libs.get(name, None) | |
141 if dep is not None: | |
142 if includeLibs and not dep in deps: | |
143 deps.append(dep) | |
144 else: | |
145 dep = pdb.project(name) | |
146 if not dep in deps: | |
147 dep.all_deps(deps, pdb, includeLibs) | |
148 if not self in deps: | |
149 deps.append(self) | |
150 return deps | |
151 | |
152 def _compute_max_dep_distances(self, name, distances, dist, pdb): | |
153 currentDist = distances.get(name); | |
154 if currentDist is None or currentDist < dist: | |
155 distances[name] = dist | |
156 if pdb.projects.has_key(name): | |
157 p = pdb.project(name) | |
158 for dep in p.deps: | |
159 self._compute_max_dep_distances(dep, distances, dist + 1, pdb) | |
160 | |
161 | |
162 def canonical_deps(self, env, pdb): | |
163 distances = dict() | |
164 result = set() | |
165 self._compute_max_dep_distances(self.name, distances, 0, pdb) | |
166 for n,d in distances.iteritems(): | |
167 assert d > 0 or n == self.name | |
168 if d == 1: | |
169 result.add(n) | |
170 | |
171 | |
172 if len(result) == len(self.deps) and frozenset(self.deps) == result: | |
173 return self.deps | |
174 return result; | |
175 | |
176 | |
177 def source_dirs(self): | |
178 return [join(self.baseDir, self.name, s) for s in self.srcDirs] | |
179 | |
180 def output_dir(self): | |
181 return join(self.baseDir, self.name, 'bin') | |
182 | |
183 def classpath(self, resolve, env): | |
184 classesDir = join(self.baseDir, 'classes') | |
185 if exists(classesDir): | |
186 return [self.output_dir(), classesDir] | |
187 return [self.output_dir()] | |
188 | |
189 | |
190 | |
191 class Library(Dependency): | |
192 def __init__(self, baseDir, name, path, mustExist, urls): | |
193 Dependency.__init__(self, name, baseDir) | |
194 self.path = path | |
195 self.urls = urls | |
196 self.mustExist = mustExist | |
197 | |
198 def classpath(self, resolve, env): | |
199 path = self.path | |
200 if not isabs(path): | |
201 path = join(self.baseDir, path) | |
202 if resolve and self.mustExist and not exists(path): | |
203 assert not len(self.urls) == 0, 'cannot find required library ' + self.name + " " + path; | |
204 env.download(path, self.urls) | |
205 | |
206 if exists(path) or not resolve: | |
207 return [path] | |
208 return [] | |
209 | |
210 class ProjectsDB(): | |
211 | |
212 def __init__(self, env): | |
213 self.env = env | |
214 self.projects = dict() | |
215 self.libs = dict() | |
216 self.commandModules = dict() | |
217 self.baseDirs = [] | |
218 self.primary = '' | |
219 | |
220 def _load_projects(self, mxDir, baseDir): | |
221 env = self.env | |
222 libsMap = dict() | |
223 projsMap = dict() | |
224 projectsFile = join(mxDir, 'projects') | |
225 if not exists(projectsFile): | |
226 return | |
227 with open(projectsFile) as f: | |
228 for line in f: | |
229 line = line.strip() | |
230 if len(line) != 0 and line[0] != '#': | |
231 key, value = line.split('=', 1) | |
232 | |
233 parts = key.split('@') | |
234 if len(parts) != 3: | |
235 env.abort('Property name does not have 3 parts separated by "@": ' + key) | |
236 kind, name, attr = parts | |
237 if kind == 'project': | |
238 m = projsMap | |
239 elif kind == 'library': | |
240 m = libsMap | |
241 else: | |
242 env.abort('Property name does not start with "project@" or "library@": ' + key) | |
243 | |
244 attrs = m.get(name) | |
245 if attrs is None: | |
246 attrs = dict() | |
247 m[name] = attrs | |
248 value = env.expandvars_in_property(value) | |
249 attrs[attr] = value | |
250 | |
251 def pop_list(attrs, name): | |
252 v = attrs.pop(name, None) | |
253 if v is None or len(v.strip()) == 0: | |
254 return [] | |
255 return [n.strip() for n in v.split(',')] | |
256 | |
257 for name, attrs in projsMap.iteritems(): | |
258 if self.projects.has_key(name): | |
259 env.abort('cannot override project ' + name + ' in ' + self.project(name).baseDir + " with project of the same name in " + mxDir) | |
260 srcDirs = pop_list(attrs, 'sourceDirs') | |
261 deps = pop_list(attrs, 'dependencies') | |
262 p = Project(baseDir, name, srcDirs, deps) | |
263 p.checkstyleProj = attrs.pop('checkstyle', name) | |
264 p.native = attrs.pop('native', '') == 'true' | |
265 p.__dict__.update(attrs) | |
266 self.projects[name] = p | |
267 | |
268 for name, attrs in libsMap.iteritems(): | |
269 if self.libs.has_key(name): | |
270 env.abort('cannot redefine library ' + name) | |
271 | |
272 path = attrs['path'] | |
273 mustExist = attrs.pop('optional', 'false') != 'true' | |
274 urls = pop_list(attrs, 'urls') | |
275 l = Library(baseDir, name, path, mustExist, urls) | |
276 l.__dict__.update(attrs) | |
277 self.libs[name] = l | |
278 | |
279 def _load_commands(self, mxDir, baseDir): | |
280 env = self.env | |
281 commands = join(mxDir, 'commands.py') | |
282 if exists(commands): | |
283 # temporarily extend the Python path | |
284 sys.path.insert(0, mxDir) | |
285 | |
286 mod = __import__('commands') | |
287 | |
288 # revert the Python path | |
289 del sys.path[0] | |
290 | |
291 if not hasattr(mod, 'mx_init'): | |
292 env.abort(commands + ' must define an mx_init(env) function') | |
293 | |
294 mod.mx_init(env) | |
295 | |
296 name = baseDir + '.commands' | |
297 sfx = 1 | |
298 while sys.modules.has_key(name): | |
299 name = baseDir + str(sfx) + '.commands' | |
300 sfx += 1 | |
301 | |
302 sys.modules[name] = sys.modules.pop('commands') | |
303 self.commandModules[name] = mod | |
304 | |
305 def _load_includes(self, mxDir, baseDir): | |
306 includes = join(mxDir, 'includes') | |
307 if exists(includes): | |
308 with open(includes) as f: | |
309 for line in f: | |
310 includeMxDir = join(self.env.expandvars_in_property(line.strip()), 'mx') | |
311 self.load(includeMxDir) | |
312 | |
313 def _load_env(self, mxDir, baseDir): | |
314 env = join(mxDir, 'env') | |
315 if exists(env): | |
316 with open(env) as f: | |
317 for line in f: | |
318 line = line.strip() | |
319 if len(line) != 0 and line[0] != '#': | |
320 key, value = line.split('=', 1) | |
321 os.environ[key.strip()] = self.env.expandvars_in_property(value.strip()) | |
322 | |
323 def load(self, mxDir, primary=False): | |
324 """ loads the mx data from a given directory """ | |
325 if not exists(mxDir) or not isdir(mxDir): | |
326 self.env.abort('Directory does not exist: ' + mxDir) | |
327 baseDir = dirname(mxDir) | |
328 if primary: | |
329 self.primary = baseDir | |
330 if not baseDir in self.baseDirs: | |
331 self.baseDirs.append(baseDir) | |
332 self._load_includes(mxDir, baseDir) | |
333 self._load_projects(mxDir, baseDir) | |
334 self._load_env(mxDir, baseDir) | |
335 if primary: | |
336 self._load_commands(mxDir, baseDir) | |
337 | |
338 def project_names(self): | |
339 return ' '.join(self.projects.keys()) | |
340 | |
341 def project(self, name, fatalIfMissing=True): | |
342 p = self.projects.get(name) | |
343 if p is None: | |
344 self.env.abort('project named ' + name + ' not found') | |
345 return p | |
346 | |
347 def library(self, name): | |
348 l = self.libs.get(name) | |
349 if l is None: | |
350 self.env.abort('library named ' + name + ' not found') | |
351 return l | |
352 | |
353 def _as_classpath(self, deps, resolve): | |
354 cp = [] | |
355 if self.env.cp_prefix is not None: | |
356 cp = [self.env.cp_prefix] | |
357 for d in deps: | |
358 cp += d.classpath(resolve, self.env) | |
359 if self.env.cp_suffix is not None: | |
360 cp += [self.env.cp_suffix] | |
361 return os.pathsep.join(cp) | |
362 | |
363 def classpath(self, names=None, resolve=True): | |
364 if names is None: | |
365 return self._as_classpath(self.sorted_deps(True), resolve) | |
366 deps = [] | |
367 if isinstance(names, types.StringTypes): | |
368 self.project(names).all_deps(deps, self, True) | |
369 else: | |
370 for n in names: | |
371 self.project(n).all_deps(deps, self, True) | |
372 return self._as_classpath(deps, resolve) | |
373 | |
374 def sorted_deps(self, includeLibs=False): | |
375 deps = [] | |
376 for p in self.projects.itervalues(): | |
377 p.all_deps(deps, self, includeLibs) | |
378 return deps | |
379 | |
380 class Env(ArgumentParser): | |
381 | |
382 def format_commands(self): | |
383 msg = '\navailable commands:\n\n' | |
384 for cmd in sorted(self.commands.iterkeys()): | |
385 c, _ = self.commands[cmd][:2] | |
386 doc = c.__doc__ | |
387 if doc is None: | |
388 doc = '' | |
389 msg += ' {0:<20} {1}\n'.format(cmd, doc.split('\n', 1)[0]) | |
390 return msg + '\n' | |
391 | |
392 # Override parent to append the list of available commands | |
393 def format_help(self): | |
394 return ArgumentParser.format_help(self) + self.format_commands() | |
395 | |
396 | |
397 def __init__(self): | |
398 self.java_initialized = False | |
399 self.pdb = ProjectsDB(self) | |
400 self.commands = dict() | |
401 ArgumentParser.__init__(self, prog='mx') | |
402 | |
403 self.add_argument('-v', action='store_true', dest='verbose', help='enable verbose output') | |
404 self.add_argument('-d', action='store_true', dest='java_dbg', help='make Java processes wait on port 8000 for a debugger') | |
405 self.add_argument('--cp-pfx', dest='cp_prefix', help='class path prefix', metavar='<arg>') | |
406 self.add_argument('--cp-sfx', dest='cp_suffix', help='class path suffix', metavar='<arg>') | |
407 self.add_argument('--J', dest='java_args', help='Java VM arguments (e.g. --J @-dsa)', metavar='@<args>', default=DEFAULT_JAVA_ARGS) | |
408 self.add_argument('--Jp', action='append', dest='java_args_pfx', help='prefix Java VM arguments (e.g. --Jp @-dsa)', metavar='@<args>', default=[]) | |
409 self.add_argument('--Ja', action='append', dest='java_args_sfx', help='suffix Java VM arguments (e.g. --Ja @-dsa)', metavar='@<args>', default=[]) | |
410 self.add_argument('--user-home', help='users home directory', metavar='<path>', default=os.path.expanduser('~')) | |
411 self.add_argument('--java-home', help='JDK installation directory (must be JDK 6 or later)', metavar='<path>', default=self.default_java_home()) | |
412 self.add_argument('--java', help='Java VM executable (default: bin/java under $JAVA_HOME)', metavar='<path>') | |
413 self.add_argument('--os', dest='os', help='operating system override') | |
414 | |
415 def _parse_cmd_line(self, args=None): | |
416 if args is None: | |
417 args = sys.argv[1:] | |
418 | |
419 self.add_argument('commandAndArgs', nargs=REMAINDER, metavar='command args...') | |
420 | |
421 self.parse_args(namespace=self) | |
422 | |
423 if self.java_home is None or self.java_home == '': | |
424 self.abort('Could not find Java home. Use --java-home option or ensure JAVA_HOME environment variable is set.') | |
425 | |
426 if self.user_home is None or self.user_home == '': | |
427 self.abort('Could not find user home. Use --user-home option or ensure HOME environment variable is set.') | |
428 | |
429 if self.os is None: | |
430 self.remote = False | |
431 if sys.platform.startswith('darwin'): | |
432 self.os = 'darwin' | |
433 elif sys.platform.startswith('linux'): | |
434 self.os = 'linux' | |
435 elif sys.platform.startswith('sunos'): | |
436 self.os = 'solaris' | |
437 elif sys.platform.startswith('win32') or sys.platform.startswith('cygwin'): | |
438 self.os = 'windows' | |
439 else: | |
440 print 'Supported operating system could not be derived from', sys.platform, '- use --os option explicitly.' | |
441 sys.exit(1) | |
442 else: | |
443 self.java_args += ' -Dmax.os=' + self.os | |
444 self.remote = True | |
445 | |
446 if self.java is None: | |
447 self.java = join(self.java_home, 'bin', 'java') | |
448 | |
449 os.environ['JAVA_HOME'] = self.java_home | |
450 os.environ['HOME'] = self.user_home | |
451 | |
452 self.javac = join(self.java_home, 'bin', 'javac') | |
453 | |
454 for mod in self.pdb.commandModules.itervalues(): | |
455 if hasattr(mod, 'mx_post_parse_cmd_line'): | |
456 mod.mx_post_parse_cmd_line(self) | |
457 | |
458 def expandvars_in_property(self, value): | |
459 result = expandvars(value) | |
460 if '$' in result or '%' in result: | |
461 self.abort('Property contains an undefined environment variable: ' + value) | |
462 return result | |
463 | |
464 | |
465 def load_config_file(self, configFile, override=False): | |
466 """ adds attributes to this object from a file containing key=value lines """ | |
467 if exists(configFile): | |
468 with open(configFile) as f: | |
469 for line in f: | |
470 k, v = line.split('=', 1) | |
471 k = k.strip().lower() | |
472 if (override or not hasattr(self, k)): | |
473 setattr(self, k, self.expandvars_in_property(v.strip())) | |
474 | |
475 def format_java_cmd(self, args): | |
476 self.init_java() | |
477 return [self.java] + self.java_args_pfx + self.java_args + self.java_args_sfx + args | |
478 | |
479 def run_java(self, args, nonZeroIsFatal=True, out=None, err=None, cwd=None): | |
480 return self.run(self.format_java_cmd(args), nonZeroIsFatal=nonZeroIsFatal, out=out, err=err, cwd=cwd) | |
481 | |
482 def run(self, args, nonZeroIsFatal=True, out=None, err=None, cwd=None): | |
483 """ | |
484 Run a command in a subprocess, wait for it to complete and return the exit status of the process. | |
485 If the exit status is non-zero and `nonZeroIsFatal` is true, then the program is exited with | |
486 the same exit status. | |
487 Each line of the standard output and error streams of the subprocess are redirected to the | |
488 provided out and err functions if they are not None. | |
489 """ | |
490 | |
491 assert isinstance(args, types.ListType), "'args' must be a list: " + str(args) | |
492 for arg in args: | |
493 assert isinstance(arg, types.StringTypes), 'argument is not a string: ' + str(arg) | |
494 | |
495 if self.verbose: | |
496 self.log(' '.join(args)) | |
497 | |
498 try: | |
499 if out is None and err is None: | |
500 retcode = subprocess.call(args, cwd=cwd) | |
501 else: | |
502 def redirect(stream, f): | |
503 for line in iter(stream.readline, ''): | |
504 f(line) | |
505 stream.close() | |
506 p = subprocess.Popen(args, cwd=cwd, stdout=None if out is None else subprocess.PIPE, stderr=None if err is None else subprocess.PIPE) | |
507 if out is not None: | |
508 t = Thread(target=redirect, args=(p.stdout, out)) | |
509 t.daemon = True # thread dies with the program | |
510 t.start() | |
511 if err is not None: | |
512 t = Thread(target=redirect, args=(p.stderr, err)) | |
513 t.daemon = True # thread dies with the program | |
514 t.start() | |
515 retcode = p.wait() | |
516 except OSError as e: | |
517 self.log('Error executing \'' + ' '.join(args) + '\': ' + str(e)) | |
518 if self.verbose: | |
519 raise e | |
520 self.abort(e.errno) | |
521 | |
522 | |
523 if retcode and nonZeroIsFatal: | |
524 if self.verbose: | |
525 raise subprocess.CalledProcessError(retcode, ' '.join(args)) | |
526 self.abort(retcode) | |
527 | |
528 return retcode | |
529 | |
530 def check_get_env(self, key): | |
531 """ | |
532 Gets an environment variable, aborting with a useful message if it is not set. | |
533 """ | |
534 value = os.environ.get(key) | |
535 if value is None: | |
536 self.abort('Required environment variable ' + key + ' must be set (e.g. in ' + join(self.pdb.primary, 'env') + ')') | |
537 return value | |
538 | |
539 def exe_suffix(self, name): | |
540 """ | |
541 Gets the platform specific suffix for an executable | |
542 """ | |
543 if self.os == 'windows': | |
544 return name + '.exe' | |
545 return name | |
546 | |
547 def log(self, msg=None): | |
548 """ | |
549 Write a message to the console. | |
550 All script output goes through this method thus allowing a subclass | |
551 to redirect it. | |
552 """ | |
553 if msg is None: | |
554 print | |
555 else: | |
556 print msg | |
557 | |
558 def expand_project_in_class_path_arg(self, cpArg): | |
559 cp = [] | |
560 for part in cpArg.split(os.pathsep): | |
561 if part.startswith('@'): | |
562 cp += self.pdb.classpath(part[1:]).split(os.pathsep) | |
563 else: | |
564 cp.append(part) | |
565 return os.pathsep.join(cp) | |
566 | |
567 def expand_project_in_args(self, args): | |
568 for i in range(len(args)): | |
569 if args[i] == '-cp' or args[i] == '-classpath': | |
570 if i + 1 < len(args): | |
571 args[i + 1] = self.expand_project_in_class_path_arg(args[i + 1]) | |
572 return | |
573 | |
574 | |
575 def init_java(self): | |
576 """ | |
577 Lazy initialization and preprocessing of this object's fields before running a Java command. | |
578 """ | |
579 if self.java_initialized: | |
580 return | |
581 | |
582 def delAtAndSplit(s): | |
583 return shlex.split(s.lstrip('@')) | |
584 | |
585 self.java_args = delAtAndSplit(self.java_args) | |
586 self.java_args_pfx = sum(map(delAtAndSplit, self.java_args_pfx), []) | |
587 self.java_args_sfx = sum(map(delAtAndSplit, self.java_args_sfx), []) | |
588 | |
589 # Prepend the -d64 VM option only if the java command supports it | |
590 output = '' | |
591 try: | |
592 output = subprocess.check_output([self.java, '-d64', '-version'], stderr=subprocess.STDOUT) | |
593 self.java_args = ['-d64'] + self.java_args | |
594 except subprocess.CalledProcessError as e: | |
595 try: | |
596 output = subprocess.check_output([self.java, '-version'], stderr=subprocess.STDOUT) | |
597 except subprocess.CalledProcessError as e: | |
598 print e.output | |
599 self.abort(e.returncode) | |
600 | |
601 output = output.split() | |
602 assert output[0] == 'java' or output[0] == 'openjdk' | |
603 assert output[1] == 'version' | |
604 version = output[2] | |
605 if not version.startswith('"1.6') and not version.startswith('"1.7'): | |
606 self.abort('Requires Java version 1.6 or 1.7, got version ' + version) | |
607 | |
608 if self.java_dbg: | |
609 self.java_args += ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000'] | |
610 | |
611 self.java_initialized = True | |
612 | |
613 def default_java_home(self): | |
614 javaHome = os.getenv('JAVA_HOME') | |
615 if javaHome is None: | |
616 if exists('/usr/lib/java/java-6-sun'): | |
617 javaHome = '/usr/lib/java/java-6-sun' | |
618 elif exists('/System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home'): | |
619 javaHome = '/System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home' | |
620 elif exists('/usr/jdk/latest'): | |
621 javaHome = '/usr/jdk/latest' | |
622 return javaHome | |
623 | |
624 def gmake_cmd(self): | |
625 for a in ['make', 'gmake', 'gnumake']: | |
626 try: | |
627 output = subprocess.check_output([a, '--version']) | |
628 if 'GNU' in output: | |
629 return a; | |
630 except: | |
631 pass | |
632 self.abort('Could not find a GNU make executable on the current path.') | |
633 | |
634 | |
635 def abort(self, codeOrMessage): | |
636 """ | |
637 Aborts the program with a SystemExit exception. | |
638 If 'codeOrMessage' is a plain integer, it specifies the system exit status; | |
639 if it is None, the exit status is zero; if it has another type (such as a string), | |
640 the object's value is printed and the exit status is one. | |
641 """ | |
642 raise SystemExit(codeOrMessage) | |
643 | |
644 def download(self, path, urls): | |
645 """ | |
646 Attempts to downloads content for each URL in a list, stopping after the first successful download. | |
647 If the content cannot be retrieved from any URL, the program is aborted. The downloaded content | |
648 is written to the file indicated by 'path'. | |
649 """ | |
650 d = dirname(path) | |
651 if d != '' and not exists(d): | |
652 os.makedirs(d) | |
653 | |
654 def url_open(url): | |
655 userAgent = 'Mozilla/5.0 (compatible)' | |
656 headers = { 'User-Agent' : userAgent } | |
657 req = urllib2.Request(url, headers=headers) | |
658 return urllib2.urlopen(req); | |
659 | |
660 for url in urls: | |
661 try: | |
662 self.log('Downloading ' + url + ' to ' + path) | |
663 if url.startswith('zip:') or url.startswith('jar:'): | |
664 i = url.find('!/') | |
665 if i == -1: | |
666 self.abort('Zip or jar URL does not contain "!/": ' + url) | |
667 url, _, entry = url[len('zip:'):].partition('!/') | |
668 with contextlib.closing(url_open(url)) as f: | |
669 data = f.read() | |
670 zipdata = StringIO.StringIO(f.read()) | |
671 | |
672 zf = zipfile.ZipFile(zipdata, 'r') | |
673 data = zf.read(entry) | |
674 with open(path, 'w') as f: | |
675 f.write(data) | |
676 else: | |
677 with contextlib.closing(url_open(url)) as f: | |
678 data = f.read() | |
679 with open(path, 'w') as f: | |
680 f.write(data) | |
681 return | |
682 except IOError as e: | |
683 self.log('Error reading from ' + url + ': ' + str(e)) | |
684 except zipfile.BadZipfile as e: | |
685 self.log('Error in zip file downloaded from ' + url + ': ' + str(e)) | |
686 | |
687 # now try it with Java - urllib2 does not handle meta refreshes which are used by Sourceforge | |
688 myDir = dirname(__file__) | |
689 | |
690 javaSource = join(myDir, 'URLConnectionDownload.java') | |
691 javaClass = join(myDir, 'URLConnectionDownload.class') | |
692 if not exists(javaClass) or getmtime(javaClass) < getmtime(javaSource): | |
693 subprocess.check_call([self.javac, '-d', myDir, javaSource]) | |
694 if self.run([self.java, '-cp', myDir, 'URLConnectionDownload', path] + urls) != 0: | |
695 self.abort('Could not download to ' + path + ' from any of the following URLs:\n\n ' + | |
696 '\n '.join(urls) + '\n\nPlease use a web browser to do the download manually') | |
697 | |
698 def update_file(self, path, content): | |
699 """ | |
700 Updates a file with some given content if the content differs from what's in | |
701 the file already. The return value indicates if the file was updated. | |
702 """ | |
703 existed = exists(path) | |
704 try: | |
705 old = None | |
706 if existed: | |
707 with open(path) as f: | |
708 old = f.read() | |
709 | |
710 if old == content: | |
711 return False | |
712 | |
713 with open(path, 'w') as f: | |
714 f.write(content) | |
715 | |
716 self.log(('modified ' if existed else 'created ') + path) | |
717 return True; | |
718 except IOError as e: | |
719 self.abort('Error while writing to ' + path + ': ' + str(e)); | |
720 | |
721 # Builtin commands | |
722 | |
723 def build(env, args): | |
724 """compile the Java and C sources, linking the latter | |
725 | |
726 Compile all the Java source code using the appropriate compilers | |
727 and linkers for the various source code types.""" | |
728 | |
729 parser = ArgumentParser(prog='mx build'); | |
730 parser.add_argument('-f', action='store_true', dest='force', help='force compilation even if class files are up to date') | |
731 parser.add_argument('-c', action='store_true', dest='clean', help='removes existing build output') | |
732 parser.add_argument('--no-native', action='store_false', dest='native', help='do not build com.oracle.max.vm.native') | |
733 parser.add_argument('--jdt', help='Eclipse installation or path to ecj.jar for using the Eclipse batch compiler instead of javac', metavar='<path>') | |
734 | |
735 args = parser.parse_args(args) | |
736 | |
737 jdtJar = None | |
738 if args.jdt is not None: | |
739 if args.jdt.endswith('.jar'): | |
740 jdtJar=args.jdt | |
741 elif isdir(args.jdt): | |
742 plugins = join(args.jdt, 'plugins') | |
743 choices = [f for f in os.listdir(plugins) if fnmatch.fnmatch(f, 'org.eclipse.jdt.core_*.jar')] | |
744 if len(choices) != 0: | |
745 jdtJar = join(plugins, sorted(choices, reverse=True)[0]) | |
746 | |
747 projects = [p.name for p in env.pdb.sorted_deps()] | |
748 built = set() | |
749 for project in projects: | |
750 p = env.pdb.project(project) | |
751 projectDir = join(p.baseDir, project) | |
752 | |
753 if p.native: | |
754 if env.os == 'windows': | |
755 env.log('Skipping C compilation on Windows until it is supported') | |
756 pass | |
757 | |
758 env.log('Compiling C sources in {0}...'.format(projectDir)) | |
759 | |
760 if args.clean: | |
761 env.run([env.gmake_cmd(), 'clean'], cwd=projectDir) | |
762 | |
763 env.run([env.gmake_cmd()], cwd=projectDir) | |
764 built.add(project) | |
765 continue | |
766 | |
767 outputDir = p.output_dir() | |
768 if exists(outputDir): | |
769 if args.clean: | |
770 env.log('Cleaning {0}...'.format(outputDir)) | |
771 shutil.rmtree(outputDir) | |
772 os.mkdir(outputDir) | |
773 else: | |
774 os.mkdir(outputDir) | |
775 | |
776 classpath = env.pdb.classpath(project) | |
777 sourceDirs = env.pdb.project(project).source_dirs() | |
778 mustBuild = args.force | |
779 if not mustBuild: | |
780 for dep in p.all_deps([], env.pdb, False): | |
781 if dep.name in built: | |
782 mustBuild = True | |
783 | |
784 for sourceDir in sourceDirs: | |
785 javafilelist = [] | |
786 nonjavafilelist = [] | |
787 for root, _, files in os.walk(sourceDir): | |
788 javafiles = [join(root, name) for name in files if name.endswith('.java') and name != 'package-info.java'] | |
789 javafilelist += javafiles | |
790 nonjavafilelist += [join(root, name) for name in files if not name.endswith('.java')] | |
791 if not mustBuild: | |
792 for javafile in javafiles: | |
793 classfile = outputDir + javafile[len(sourceDir):-len('java')] + 'class' | |
794 if not exists(classfile) or os.path.getmtime(javafile) > os.path.getmtime(classfile): | |
795 mustBuild = True | |
796 break | |
797 | |
798 if not mustBuild: | |
799 env.log('[all class files in {0} are up to date - skipping]'.format(sourceDir)) | |
800 continue | |
801 | |
802 if len(javafilelist) == 0: | |
803 env.log('[no Java sources in {0} - skipping]'.format(sourceDir)) | |
804 continue | |
805 | |
806 built.add(project) | |
807 | |
808 argfileName = join(projectDir, 'javafilelist.txt') | |
809 argfile = open(argfileName, 'w') | |
810 argfile.write('\n'.join(javafilelist)) | |
811 argfile.close() | |
812 | |
813 try: | |
814 if jdtJar is None: | |
815 env.log('Compiling Java sources in {0} with javac...'.format(sourceDir)) | |
816 | |
817 class Filter: | |
818 """ | |
819 Class to filter the 'is Sun proprietary API and may be removed in a future release' | |
820 warning when compiling the VM classes. | |
821 | |
822 """ | |
823 def __init__(self): | |
824 self.c = 0 | |
825 | |
826 def eat(self, line): | |
827 if 'Sun proprietary API' in line: | |
828 self.c = 2 | |
829 elif self.c != 0: | |
830 self.c -= 1 | |
831 else: | |
832 print line.rstrip() | |
833 | |
834 env.run([env.javac, '-g', '-J-Xmx1g', '-classpath', classpath, '-d', outputDir, '@' + argfile.name], err=Filter().eat) | |
835 else: | |
836 env.log('Compiling Java sources in {0} with JDT...'.format(sourceDir)) | |
837 jdtProperties = join(projectDir, '.settings', 'org.eclipse.jdt.core.prefs') | |
838 if not exists(jdtProperties): | |
839 raise SystemError('JDT properties file {0} not found'.format(jdtProperties)) | |
840 env.run([env.java, '-Xmx1g', '-jar', jdtJar, '-1.6', '-cp', classpath, '-g', | |
841 '-properties', jdtProperties, | |
842 '-warn:-unusedImport,-unchecked', | |
843 '-d', outputDir, '@' + argfile.name]) | |
844 finally: | |
845 os.remove(argfileName) | |
846 | |
847 | |
848 for name in nonjavafilelist: | |
849 dst = join(outputDir, name[len(sourceDir) + 1:]) | |
850 if exists(dirname(dst)): | |
851 shutil.copyfile(name, dst) | |
852 | |
853 def canonicalizeprojects(env, args): | |
854 """process all project files to canonicalize the dependencies | |
855 | |
856 The exit code of this command reflects how many files were updated.""" | |
857 | |
858 changedFiles = 0 | |
859 pdb = env.pdb | |
860 for d in pdb.baseDirs: | |
861 projectsFile = join(d, 'mx', 'projects') | |
862 if not exists(projectsFile): | |
863 continue | |
864 with open(projectsFile) as f: | |
865 out = StringIO.StringIO() | |
866 pattern = re.compile('project@([^@]+)@dependencies=.*') | |
867 for line in f: | |
868 line = line.strip() | |
869 m = pattern.match(line) | |
870 if m is None: | |
871 out.write(line + '\n') | |
872 else: | |
873 p = pdb.project(m.group(1)) | |
874 out.write('project@' + m.group(1) + '@dependencies=' + ','.join(p.canonical_deps(env, pdb)) + '\n') | |
875 content = out.getvalue() | |
876 if env.update_file(projectsFile, content): | |
877 changedFiles += 1 | |
878 return changedFiles; | |
879 | |
880 def checkstyle(env, args): | |
881 """run Checkstyle on the Java sources | |
882 | |
883 Run Checkstyle over the Java sources. Any errors or warnings | |
884 produced by Checkstyle result in a non-zero exit code. | |
885 | |
886 If no projects are given, then all Java projects are checked.""" | |
887 | |
888 allProjects = [p.name for p in env.pdb.sorted_deps()] | |
889 if len(args) == 0: | |
890 projects = allProjects | |
891 else: | |
892 projects = args | |
893 unknown = set(projects).difference(allProjects) | |
894 if len(unknown) != 0: | |
895 env.error('unknown projects: ' + ', '.join(unknown)) | |
896 | |
897 for project in projects: | |
898 p = env.pdb.project(project) | |
899 projectDir = join(p.baseDir, project) | |
900 sourceDirs = env.pdb.project(project).source_dirs() | |
901 dotCheckstyle = join(projectDir, '.checkstyle') | |
902 | |
903 if not exists(dotCheckstyle): | |
904 continue | |
905 | |
906 for sourceDir in sourceDirs: | |
907 javafilelist = [] | |
908 for root, _, files in os.walk(sourceDir): | |
909 javafilelist += [join(root, name) for name in files if name.endswith('.java') and name != 'package-info.java'] | |
910 if len(javafilelist) == 0: | |
911 env.log('[no Java sources in {0} - skipping]'.format(sourceDir)) | |
912 continue | |
913 | |
914 timestampFile = join(p.baseDir, 'mx', '.checkstyle' + sourceDir[len(p.baseDir):].replace(os.sep, '_') + '.timestamp') | |
915 mustCheck = False | |
916 if exists(timestampFile): | |
917 timestamp = os.path.getmtime(timestampFile) | |
918 for f in javafilelist: | |
919 if os.path.getmtime(f) > timestamp: | |
920 mustCheck = True | |
921 break | |
922 else: | |
923 mustCheck = True | |
924 | |
925 if not mustCheck: | |
926 env.log('[all Java sources in {0} already checked - skipping]'.format(sourceDir)) | |
927 continue | |
928 | |
929 if exists(timestampFile): | |
930 os.utime(timestampFile, None) | |
931 else: | |
932 file(timestampFile, 'a') | |
933 | |
934 dotCheckstyleXML = xml.dom.minidom.parse(dotCheckstyle) | |
935 localCheckConfig = dotCheckstyleXML.getElementsByTagName('local-check-config')[0] | |
936 configLocation = localCheckConfig.getAttribute('location') | |
937 if configLocation.startswith('/'): | |
938 config = join(p.baseDir, configLocation.lstrip('/')) | |
939 else: | |
940 config = join(projectDir, configLocation) | |
941 | |
942 exclude = join(projectDir, '.checkstyle.exclude') | |
943 | |
944 | |
945 | |
946 | |
947 if exists(exclude): | |
948 with open(exclude) as f: | |
949 # Convert patterns to OS separators | |
950 patterns = [name.rstrip().replace('/', os.sep) for name in f.readlines()] | |
951 def match(name): | |
952 for p in patterns: | |
953 if p in name: | |
954 env.log('excluding: ' + name) | |
955 return True | |
956 return False | |
957 | |
958 javafilelist = [name for name in javafilelist if not match(name)] | |
959 | |
960 auditfileName = join(projectDir, 'checkstyleOutput.txt') | |
961 env.log('Running Checkstyle on {0} using {1}...'.format(sourceDir, config)) | |
962 | |
963 try: | |
964 | |
965 # Checkstyle is unable to read the filenames to process from a file, and the | |
966 # CreateProcess function on Windows limits the length of a command line to | |
967 # 32,768 characters (http://msdn.microsoft.com/en-us/library/ms682425%28VS.85%29.aspx) | |
968 # so calling Checkstyle must be done in batches. | |
969 while len(javafilelist) != 0: | |
970 i = 0 | |
971 size = 0 | |
972 while i < len(javafilelist): | |
973 s = len(javafilelist[i]) + 1 | |
974 if (size + s < 30000): | |
975 size += s | |
976 i += 1 | |
977 else: | |
978 break | |
979 | |
980 batch = javafilelist[:i] | |
981 javafilelist = javafilelist[i:] | |
982 try: | |
983 env.run_java(['-Xmx1g', '-jar', env.pdb.library('CHECKSTYLE').classpath(True, env)[0], '-c', config, '-o', auditfileName] + batch) | |
984 finally: | |
985 if exists(auditfileName): | |
986 with open(auditfileName) as f: | |
987 warnings = [line.strip() for line in f if 'warning:' in line] | |
988 if len(warnings) != 0: | |
989 map(env.log, warnings) | |
990 return 1 | |
991 finally: | |
992 if exists(auditfileName): | |
993 os.unlink(auditfileName) | |
994 return 0 | |
995 | |
996 def clean(env, args): | |
997 """remove all class files, images, and executables | |
998 | |
999 Removes all files created by a build, including Java class files, executables, and | |
1000 generated images. | |
1001 """ | |
1002 | |
1003 projects = env.pdb.projects.keys() | |
1004 for project in projects: | |
1005 p = env.pdb.project(project) | |
1006 if p.native: | |
1007 env.run([env.gmake_cmd(), '-C', p.dir, 'clean']) | |
1008 else: | |
1009 outputDir = p.output_dir() | |
1010 if outputDir != '' and exists(outputDir): | |
1011 env.log('Removing {0}...'.format(outputDir)) | |
1012 shutil.rmtree(outputDir) | |
1013 | |
1014 def help_(env, args): | |
1015 """show help for a given command | |
1016 | |
1017 With no arguments, print a list of commands and short help for each command. | |
1018 | |
1019 Given a command name, print help for that command.""" | |
1020 if len(args) == 0: | |
1021 env.print_help() | |
1022 return | |
1023 | |
1024 name = args[0] | |
1025 if not env.commands.has_key(name): | |
1026 env.error('unknown command: ' + name) | |
1027 | |
1028 value = env.commands[name] | |
1029 (func, usage) = value[:2] | |
1030 doc = func.__doc__ | |
1031 if len(value) > 2: | |
1032 docArgs = value[2:] | |
1033 fmtArgs = [] | |
1034 for d in docArgs: | |
1035 if isinstance(d, Callable): | |
1036 fmtArgs += [d(env)] | |
1037 else: | |
1038 fmtArgs += [str(d)] | |
1039 doc = doc.format(*fmtArgs) | |
1040 print 'mx {0} {1}\n\n{2}\n'.format(name, usage, doc) | |
1041 | |
1042 | |
1043 # Commands are in alphabetical order in this file. | |
1044 | |
1045 def javap(env, args): | |
1046 """launch javap with a -classpath option denoting all available classes | |
1047 | |
1048 Run the JDK javap class file disassembler with the following prepended options: | |
1049 | |
1050 -private -verbose -classpath <path to project classes>""" | |
1051 | |
1052 javap = join(env.java_home, 'bin', 'javap') | |
1053 if not exists(javap): | |
1054 env.abort('The javap executable does not exists: ' + javap) | |
1055 else: | |
1056 env.run([javap, '-private', '-verbose', '-classpath', env.pdb.classpath()] + args) | |
1057 | |
1058 def projects(env, args): | |
1059 """show all loaded projects""" | |
1060 pdb = env.pdb | |
1061 for d in pdb.baseDirs: | |
1062 projectsFile = join(d, 'mx', 'projects') | |
1063 if exists(projectsFile): | |
1064 env.log('# file: ' + projectsFile) | |
1065 for p in pdb.projects.values(): | |
1066 if p.baseDir == d: | |
1067 env.log(p.name) | |
1068 | |
1069 | |
1070 def main(env): | |
1071 | |
1072 # Table of commands in alphabetical order. | |
1073 # Keys are command names, value are lists: [<function>, <usage msg>, <format args to doc string of function>...] | |
1074 # If any of the format args are instances of Callable, then they are called with an 'env' are before being | |
1075 # used in the call to str.format(). | |
1076 # Extensions should update this table directly | |
1077 env.commands = { | |
1078 'build': [build, '[options] projects...'], | |
1079 'checkstyle': [checkstyle, 'projects...'], | |
1080 'canonicalizeprojects': [canonicalizeprojects, ''], | |
1081 'clean': [clean, ''], | |
1082 'help': [help_, '[command]'], | |
1083 'javap': [javap, ''], | |
1084 'projects': [projects, ''], | |
1085 } | |
1086 | |
1087 MX_INCLUDES = os.environ.get('MX_INCLUDES', None) | |
1088 if MX_INCLUDES is not None: | |
1089 for path in MX_INCLUDES.split(os.pathsep): | |
1090 d = join(path, 'mx') | |
1091 if exists(d) and isdir(d): | |
1092 env.pdb.load(d) | |
1093 | |
1094 cwdMxDir = join(os.getcwd(), 'mx') | |
1095 if exists(cwdMxDir) and isdir(cwdMxDir): | |
1096 env.pdb.load(cwdMxDir, primary=True) | |
1097 | |
1098 env._parse_cmd_line() | |
1099 | |
1100 if len(env.commandAndArgs) == 0: | |
1101 env.print_help() | |
1102 return | |
1103 | |
1104 env.command = env.commandAndArgs[0] | |
1105 env.command_args = env.commandAndArgs[1:] | |
1106 | |
1107 if not env.commands.has_key(env.command): | |
1108 env.abort('mx: unknown command \'{0}\'\n{1}use "mx help" for more options'.format(env.command, env.format_commands())) | |
1109 | |
1110 c, _ = env.commands[env.command][:2] | |
1111 try: | |
1112 retcode = c(env, env.command_args) | |
1113 if retcode is not None and retcode != 0: | |
1114 env.abort(retcode) | |
1115 except KeyboardInterrupt: | |
1116 # no need to show the stack trace when the user presses CTRL-C | |
1117 env.abort(1) | |
1118 | |
1119 if __name__ == '__main__': | |
1120 main(Env()) |