|
1 # This Source Code Form is subject to the terms of the Mozilla Public |
|
2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 import sys |
|
6 import os |
|
7 import optparse |
|
8 import webbrowser |
|
9 import time |
|
10 |
|
11 from copy import copy |
|
12 import simplejson as json |
|
13 from cuddlefish import packaging |
|
14 from cuddlefish._version import get_versions |
|
15 |
|
16 MOZRUNNER_BIN_NOT_FOUND = 'Mozrunner could not locate your binary' |
|
17 MOZRUNNER_BIN_NOT_FOUND_HELP = """ |
|
18 I can't find the application binary in any of its default locations |
|
19 on your system. Please specify one using the -b/--binary option. |
|
20 """ |
|
21 |
|
22 UPDATE_RDF_FILENAME = "%s.update.rdf" |
|
23 XPI_FILENAME = "%s.xpi" |
|
24 |
|
25 usage = """ |
|
26 %prog [options] command [command-specific options] |
|
27 |
|
28 Supported Commands: |
|
29 init - create a sample addon in an empty directory |
|
30 test - run tests |
|
31 run - run program |
|
32 xpi - generate an xpi |
|
33 |
|
34 Internal Commands: |
|
35 testcfx - test the cfx tool |
|
36 testex - test all example code |
|
37 testpkgs - test all installed packages |
|
38 testall - test whole environment |
|
39 |
|
40 Experimental and internal commands and options are not supported and may be |
|
41 changed or removed in the future. |
|
42 """ |
|
43 |
|
44 global_options = [ |
|
45 (("-v", "--verbose",), dict(dest="verbose", |
|
46 help="enable lots of output", |
|
47 action="store_true", |
|
48 default=False)), |
|
49 ] |
|
50 |
|
51 parser_groups = ( |
|
52 ("Supported Command-Specific Options", [ |
|
53 (("", "--update-url",), dict(dest="update_url", |
|
54 help="update URL in install.rdf", |
|
55 metavar=None, |
|
56 default=None, |
|
57 cmds=['xpi'])), |
|
58 (("", "--update-link",), dict(dest="update_link", |
|
59 help="generate update.rdf", |
|
60 metavar=None, |
|
61 default=None, |
|
62 cmds=['xpi'])), |
|
63 (("-p", "--profiledir",), dict(dest="profiledir", |
|
64 help=("profile directory to pass to " |
|
65 "app"), |
|
66 metavar=None, |
|
67 default=None, |
|
68 cmds=['test', 'run', 'testex', |
|
69 'testpkgs', 'testall'])), |
|
70 (("-b", "--binary",), dict(dest="binary", |
|
71 help="path to app binary", |
|
72 metavar=None, |
|
73 default=None, |
|
74 cmds=['test', 'run', 'testex', 'testpkgs', |
|
75 'testall'])), |
|
76 (("", "--binary-args",), dict(dest="cmdargs", |
|
77 help=("additional arguments passed to the " |
|
78 "binary"), |
|
79 metavar=None, |
|
80 default=None, |
|
81 cmds=['run', 'test'])), |
|
82 (("", "--dependencies",), dict(dest="dep_tests", |
|
83 help="include tests for all deps", |
|
84 action="store_true", |
|
85 default=False, |
|
86 cmds=['test', 'testex', 'testpkgs', |
|
87 'testall'])), |
|
88 (("", "--times",), dict(dest="iterations", |
|
89 type="int", |
|
90 help="number of times to run tests", |
|
91 default=1, |
|
92 cmds=['test', 'testex', 'testpkgs', |
|
93 'testall'])), |
|
94 (("-f", "--filter",), dict(dest="filter", |
|
95 help=("only run tests whose filenames " |
|
96 "match FILENAME and optionally " |
|
97 "match TESTNAME, both regexps"), |
|
98 metavar="FILENAME[:TESTNAME]", |
|
99 default='', |
|
100 cmds=['test', 'testex', 'testaddons', 'testpkgs', |
|
101 'testall'])), |
|
102 (("-g", "--use-config",), dict(dest="config", |
|
103 help="use named config from local.json", |
|
104 metavar=None, |
|
105 default="default", |
|
106 cmds=['test', 'run', 'xpi', 'testex', |
|
107 'testpkgs', 'testall'])), |
|
108 (("", "--templatedir",), dict(dest="templatedir", |
|
109 help="XULRunner app/ext. template", |
|
110 metavar=None, |
|
111 default=None, |
|
112 cmds=['run', 'xpi'])), |
|
113 (("", "--package-path",), dict(dest="packagepath", action="append", |
|
114 help="extra directories for package search", |
|
115 metavar=None, |
|
116 default=[], |
|
117 cmds=['run', 'xpi', 'test'])), |
|
118 (("", "--extra-packages",), dict(dest="extra_packages", |
|
119 help=("extra packages to include, " |
|
120 "comma-separated. Default is " |
|
121 "'addon-sdk'."), |
|
122 metavar=None, |
|
123 default="addon-sdk", |
|
124 cmds=['run', 'xpi', 'test', 'testex', |
|
125 'testpkgs', 'testall', |
|
126 'testcfx'])), |
|
127 (("", "--pkgdir",), dict(dest="pkgdir", |
|
128 help=("package dir containing " |
|
129 "package.json; default is " |
|
130 "current directory"), |
|
131 metavar=None, |
|
132 default=None, |
|
133 cmds=['run', 'xpi', 'test'])), |
|
134 (("", "--static-args",), dict(dest="static_args", |
|
135 help="extra harness options as JSON", |
|
136 type="json", |
|
137 metavar=None, |
|
138 default="{}", |
|
139 cmds=['run', 'xpi'])), |
|
140 (("", "--parseable",), dict(dest="parseable", |
|
141 help="display test output in a parseable format", |
|
142 action="store_true", |
|
143 default=False, |
|
144 cmds=['run', 'test', 'testex', 'testpkgs', |
|
145 'testaddons', 'testall'])), |
|
146 ] |
|
147 ), |
|
148 |
|
149 ("Experimental Command-Specific Options", [ |
|
150 (("-a", "--app",), dict(dest="app", |
|
151 help=("app to run: firefox (default), fennec, " |
|
152 "fennec-on-device, xulrunner or " |
|
153 "thunderbird"), |
|
154 metavar=None, |
|
155 type="choice", |
|
156 choices=["firefox", "fennec", |
|
157 "fennec-on-device", "thunderbird", |
|
158 "xulrunner"], |
|
159 default="firefox", |
|
160 cmds=['test', 'run', 'testex', 'testpkgs', |
|
161 'testall'])), |
|
162 (("-o", "--overload-modules",), dict(dest="overload_modules", |
|
163 help=("Overload JS modules integrated into" |
|
164 " Firefox with the one from your SDK" |
|
165 " repository"), |
|
166 action="store_true", |
|
167 default=False, |
|
168 cmds=['run', 'test', 'testex', 'testpkgs', |
|
169 'testall'])), |
|
170 (("", "--strip-sdk",), dict(dest="bundle_sdk", |
|
171 help=("Do not ship SDK modules in the xpi"), |
|
172 action="store_false", |
|
173 default=False, |
|
174 cmds=['run', 'test', 'testex', 'testpkgs', |
|
175 'testall', 'xpi'])), |
|
176 (("", "--force-use-bundled-sdk",), dict(dest="force_use_bundled_sdk", |
|
177 help=("When --strip-sdk isn't passed, " |
|
178 "force using sdk modules shipped in " |
|
179 "the xpi instead of firefox ones"), |
|
180 action="store_true", |
|
181 default=False, |
|
182 cmds=['run', 'test', 'testex', 'testpkgs', |
|
183 'testall', 'xpi'])), |
|
184 (("", "--no-run",), dict(dest="no_run", |
|
185 help=("Instead of launching the " |
|
186 "application, just show the command " |
|
187 "for doing so. Use this to launch " |
|
188 "the application in a debugger like " |
|
189 "gdb."), |
|
190 action="store_true", |
|
191 default=False, |
|
192 cmds=['run', 'test'])), |
|
193 (("", "--no-strip-xpi",), dict(dest="no_strip_xpi", |
|
194 help="retain unused modules in XPI", |
|
195 action="store_true", |
|
196 default=False, |
|
197 cmds=['xpi'])), |
|
198 (("", "--force-mobile",), dict(dest="enable_mobile", |
|
199 help="Force compatibility with Firefox Mobile", |
|
200 action="store_true", |
|
201 default=False, |
|
202 cmds=['run', 'test', 'xpi', 'testall'])), |
|
203 (("", "--mobile-app",), dict(dest="mobile_app_name", |
|
204 help=("Name of your Android application to " |
|
205 "use. Possible values: 'firefox', " |
|
206 "'firefox_beta', 'fennec_aurora', " |
|
207 "'fennec' (for nightly)."), |
|
208 metavar=None, |
|
209 default=None, |
|
210 cmds=['run', 'test', 'testall'])), |
|
211 (("", "--harness-option",), dict(dest="extra_harness_option_args", |
|
212 help=("Extra properties added to " |
|
213 "harness-options.json"), |
|
214 action="append", |
|
215 metavar="KEY=VALUE", |
|
216 default=[], |
|
217 cmds=['xpi'])), |
|
218 (("", "--stop-on-error",), dict(dest="stopOnError", |
|
219 help="Stop running tests after the first failure", |
|
220 action="store_true", |
|
221 metavar=None, |
|
222 default=False, |
|
223 cmds=['test', 'testex', 'testpkgs'])), |
|
224 (("", "--check-memory",), dict(dest="check_memory", |
|
225 help="attempts to detect leaked compartments after a test run", |
|
226 action="store_true", |
|
227 default=False, |
|
228 cmds=['test', 'testpkgs', 'testaddons', |
|
229 'testall'])), |
|
230 (("", "--output-file",), dict(dest="output_file", |
|
231 help="Where to put the finished .xpi", |
|
232 default=None, |
|
233 cmds=['xpi'])), |
|
234 (("", "--manifest-overload",), dict(dest="manifest_overload", |
|
235 help="JSON file to overload package.json properties", |
|
236 default=None, |
|
237 cmds=['xpi'])), |
|
238 ] |
|
239 ), |
|
240 |
|
241 ("Internal Command-Specific Options", [ |
|
242 (("", "--addons",), dict(dest="addons", |
|
243 help=("paths of addons to install, " |
|
244 "comma-separated"), |
|
245 metavar=None, |
|
246 default=None, |
|
247 cmds=['test', 'run', 'testex', 'testpkgs', |
|
248 'testall'])), |
|
249 (("", "--test-runner-pkg",), dict(dest="test_runner_pkg", |
|
250 help=("name of package " |
|
251 "containing test runner " |
|
252 "program (default is " |
|
253 "test-harness)"), |
|
254 default="addon-sdk", |
|
255 cmds=['test', 'testex', 'testpkgs', |
|
256 'testall'])), |
|
257 # --keydir was removed in 1.0b5, but we keep it around in the options |
|
258 # parser to make life easier for frontends like FlightDeck which |
|
259 # might still pass it. It can go away once the frontends are updated. |
|
260 (("", "--keydir",), dict(dest="keydir", |
|
261 help=("obsolete, ignored"), |
|
262 metavar=None, |
|
263 default=None, |
|
264 cmds=['test', 'run', 'xpi', 'testex', |
|
265 'testpkgs', 'testall'])), |
|
266 (("", "--e10s",), dict(dest="enable_e10s", |
|
267 help="enable out-of-process Jetpacks", |
|
268 action="store_true", |
|
269 default=False, |
|
270 cmds=['test', 'run', 'testex', 'testpkgs'])), |
|
271 (("", "--logfile",), dict(dest="logfile", |
|
272 help="log console output to file", |
|
273 metavar=None, |
|
274 default=None, |
|
275 cmds=['run', 'test', 'testex', 'testpkgs'])), |
|
276 # TODO: This should default to true once our memory debugging |
|
277 # issues are resolved; see bug 592774. |
|
278 (("", "--profile-memory",), dict(dest="profileMemory", |
|
279 help=("profile memory usage " |
|
280 "(default is false)"), |
|
281 type="int", |
|
282 action="store", |
|
283 default=0, |
|
284 cmds=['test', 'testex', 'testpkgs', |
|
285 'testall'])), |
|
286 ] |
|
287 ), |
|
288 ) |
|
289 |
|
290 def find_parent_package(cur_dir): |
|
291 tail = True |
|
292 while tail: |
|
293 if os.path.exists(os.path.join(cur_dir, 'package.json')): |
|
294 return cur_dir |
|
295 cur_dir, tail = os.path.split(cur_dir) |
|
296 return None |
|
297 |
|
298 def check_json(option, opt, value): |
|
299 # We return the parsed JSON here; see bug 610816 for background on why. |
|
300 try: |
|
301 return json.loads(value) |
|
302 except ValueError: |
|
303 raise optparse.OptionValueError("Option %s must be JSON." % opt) |
|
304 |
|
305 class CfxOption(optparse.Option): |
|
306 TYPES = optparse.Option.TYPES + ('json',) |
|
307 TYPE_CHECKER = copy(optparse.Option.TYPE_CHECKER) |
|
308 TYPE_CHECKER['json'] = check_json |
|
309 |
|
310 def parse_args(arguments, global_options, usage, version, parser_groups, |
|
311 defaults=None): |
|
312 parser = optparse.OptionParser(usage=usage.strip(), option_class=CfxOption, |
|
313 version=version) |
|
314 |
|
315 def name_cmp(a, b): |
|
316 # a[0] = name sequence |
|
317 # a[0][0] = short name (possibly empty string) |
|
318 # a[0][1] = long name |
|
319 names = [] |
|
320 for seq in (a, b): |
|
321 names.append(seq[0][0][1:] if seq[0][0] else seq[0][1][2:]) |
|
322 return cmp(*names) |
|
323 |
|
324 global_options.sort(name_cmp) |
|
325 for names, opts in global_options: |
|
326 parser.add_option(*names, **opts) |
|
327 |
|
328 for group_name, options in parser_groups: |
|
329 group = optparse.OptionGroup(parser, group_name) |
|
330 options.sort(name_cmp) |
|
331 for names, opts in options: |
|
332 if 'cmds' in opts: |
|
333 cmds = opts['cmds'] |
|
334 del opts['cmds'] |
|
335 cmds.sort() |
|
336 if not 'help' in opts: |
|
337 opts['help'] = "" |
|
338 opts['help'] += " (%s)" % ", ".join(cmds) |
|
339 group.add_option(*names, **opts) |
|
340 parser.add_option_group(group) |
|
341 |
|
342 if defaults: |
|
343 parser.set_defaults(**defaults) |
|
344 |
|
345 (options, args) = parser.parse_args(args=arguments) |
|
346 |
|
347 if not args: |
|
348 parser.print_help() |
|
349 parser.exit() |
|
350 |
|
351 return (options, args) |
|
352 |
|
353 # all tests emit progress messages to stderr, not stdout. (the mozrunner |
|
354 # console output goes to stderr and is hard to change, and |
|
355 # unittest.TextTestRunner prefers stderr, so we send everything else there |
|
356 # too, to keep all the messages in order) |
|
357 |
|
358 def test_all(env_root, defaults): |
|
359 fail = False |
|
360 |
|
361 starttime = time.time() |
|
362 |
|
363 if not defaults['filter']: |
|
364 print >>sys.stderr, "Testing cfx..." |
|
365 sys.stderr.flush() |
|
366 result = test_cfx(env_root, defaults['verbose']) |
|
367 if result.failures or result.errors: |
|
368 fail = True |
|
369 |
|
370 if not fail or not defaults.get("stopOnError"): |
|
371 print >>sys.stderr, "Testing all examples..." |
|
372 sys.stderr.flush() |
|
373 |
|
374 try: |
|
375 test_all_examples(env_root, defaults) |
|
376 except SystemExit, e: |
|
377 fail = (e.code != 0) or fail |
|
378 |
|
379 if not fail or not defaults.get("stopOnError"): |
|
380 print >>sys.stderr, "Testing all unit-test addons..." |
|
381 sys.stderr.flush() |
|
382 |
|
383 try: |
|
384 test_all_testaddons(env_root, defaults) |
|
385 except SystemExit, e: |
|
386 fail = (e.code != 0) or fail |
|
387 |
|
388 if not fail or not defaults.get("stopOnError"): |
|
389 print >>sys.stderr, "Testing all packages..." |
|
390 sys.stderr.flush() |
|
391 try: |
|
392 test_all_packages(env_root, defaults) |
|
393 except SystemExit, e: |
|
394 fail = (e.code != 0) or fail |
|
395 |
|
396 print >>sys.stderr, "Total time for all tests: %f seconds" % (time.time() - starttime) |
|
397 |
|
398 if fail: |
|
399 print >>sys.stderr, "Some tests were unsuccessful." |
|
400 sys.exit(1) |
|
401 print >>sys.stderr, "All tests were successful. Ship it!" |
|
402 sys.exit(0) |
|
403 |
|
404 def test_cfx(env_root, verbose): |
|
405 import cuddlefish.tests |
|
406 |
|
407 # tests write to stderr. flush everything before and after to avoid |
|
408 # confusion later. |
|
409 sys.stdout.flush(); sys.stderr.flush() |
|
410 olddir = os.getcwd() |
|
411 os.chdir(env_root) |
|
412 retval = cuddlefish.tests.run(verbose) |
|
413 os.chdir(olddir) |
|
414 sys.stdout.flush(); sys.stderr.flush() |
|
415 return retval |
|
416 |
|
417 def test_all_testaddons(env_root, defaults): |
|
418 addons_dir = os.path.join(env_root, "test", "addons") |
|
419 addons = [dirname for dirname in os.listdir(addons_dir) |
|
420 if os.path.isdir(os.path.join(addons_dir, dirname))] |
|
421 addons.sort() |
|
422 fail = False |
|
423 for dirname in addons: |
|
424 if (not defaults['filter'].split(":")[0] in dirname): |
|
425 continue |
|
426 |
|
427 print >>sys.stderr, "Testing %s..." % dirname |
|
428 sys.stderr.flush() |
|
429 try: |
|
430 run(arguments=["run", |
|
431 "--pkgdir", |
|
432 os.path.join(addons_dir, dirname)], |
|
433 defaults=defaults, |
|
434 env_root=env_root) |
|
435 except SystemExit, e: |
|
436 fail = (e.code != 0) or fail |
|
437 if fail and defaults.get("stopOnError"): |
|
438 break |
|
439 |
|
440 if fail: |
|
441 print >>sys.stderr, "Some test addons tests were unsuccessful." |
|
442 sys.exit(-1) |
|
443 |
|
444 def test_all_examples(env_root, defaults): |
|
445 examples_dir = os.path.join(env_root, "examples") |
|
446 examples = [dirname for dirname in os.listdir(examples_dir) |
|
447 if os.path.isdir(os.path.join(examples_dir, dirname))] |
|
448 examples.sort() |
|
449 fail = False |
|
450 for dirname in examples: |
|
451 if (not defaults['filter'].split(":")[0] in dirname): |
|
452 continue |
|
453 |
|
454 print >>sys.stderr, "Testing %s..." % dirname |
|
455 sys.stderr.flush() |
|
456 try: |
|
457 run(arguments=["test", |
|
458 "--pkgdir", |
|
459 os.path.join(examples_dir, dirname)], |
|
460 defaults=defaults, |
|
461 env_root=env_root) |
|
462 except SystemExit, e: |
|
463 fail = (e.code != 0) or fail |
|
464 if fail and defaults.get("stopOnError"): |
|
465 break |
|
466 |
|
467 if fail: |
|
468 print >>sys.stderr, "Some examples tests were unsuccessful." |
|
469 sys.exit(-1) |
|
470 |
|
471 def test_all_packages(env_root, defaults): |
|
472 packages_dir = os.path.join(env_root, "packages") |
|
473 if os.path.isdir(packages_dir): |
|
474 packages = [dirname for dirname in os.listdir(packages_dir) |
|
475 if os.path.isdir(os.path.join(packages_dir, dirname))] |
|
476 else: |
|
477 packages = [] |
|
478 packages.append(env_root) |
|
479 packages.sort() |
|
480 print >>sys.stderr, "Testing all available packages: %s." % (", ".join(packages)) |
|
481 sys.stderr.flush() |
|
482 fail = False |
|
483 for dirname in packages: |
|
484 print >>sys.stderr, "Testing %s..." % dirname |
|
485 sys.stderr.flush() |
|
486 try: |
|
487 run(arguments=["test", |
|
488 "--pkgdir", |
|
489 os.path.join(packages_dir, dirname)], |
|
490 defaults=defaults, |
|
491 env_root=env_root) |
|
492 except SystemExit, e: |
|
493 fail = (e.code != 0) or fail |
|
494 if fail and defaults.get('stopOnError'): |
|
495 break |
|
496 if fail: |
|
497 print >>sys.stderr, "Some package tests were unsuccessful." |
|
498 sys.exit(-1) |
|
499 |
|
500 def get_config_args(name, env_root): |
|
501 local_json = os.path.join(env_root, "local.json") |
|
502 if not (os.path.exists(local_json) and |
|
503 os.path.isfile(local_json)): |
|
504 if name == "default": |
|
505 return [] |
|
506 else: |
|
507 print >>sys.stderr, "File does not exist: %s" % local_json |
|
508 sys.exit(1) |
|
509 local_json = packaging.load_json_file(local_json) |
|
510 if 'configs' not in local_json: |
|
511 print >>sys.stderr, "'configs' key not found in local.json." |
|
512 sys.exit(1) |
|
513 if name not in local_json.configs: |
|
514 if name == "default": |
|
515 return [] |
|
516 else: |
|
517 print >>sys.stderr, "No config found for '%s'." % name |
|
518 sys.exit(1) |
|
519 config = local_json.configs[name] |
|
520 if type(config) != list: |
|
521 print >>sys.stderr, "Config for '%s' must be a list of strings." % name |
|
522 sys.exit(1) |
|
523 return config |
|
524 |
|
525 def initializer(env_root, args, out=sys.stdout, err=sys.stderr): |
|
526 from templates import PACKAGE_JSON, TEST_MAIN_JS |
|
527 from preflight import create_jid |
|
528 path = os.getcwd() |
|
529 addon = os.path.basename(path) |
|
530 # if more than two arguments |
|
531 if len(args) > 2: |
|
532 print >>err, 'Too many arguments.' |
|
533 return {"result":1} |
|
534 if len(args) == 2: |
|
535 path = os.path.join(path,args[1]) |
|
536 try: |
|
537 os.mkdir(path) |
|
538 print >>out, '*', args[1], 'package directory created' |
|
539 except OSError: |
|
540 print >>out, '*', args[1], 'already exists, testing if directory is empty' |
|
541 # avoid clobbering existing files, but we tolerate things like .git |
|
542 existing = [fn for fn in os.listdir(path) if not fn.startswith(".")] |
|
543 if existing: |
|
544 print >>err, 'This command must be run in an empty directory.' |
|
545 return {"result":1} |
|
546 for d in ['lib','data','test']: |
|
547 os.mkdir(os.path.join(path,d)) |
|
548 print >>out, '*', d, 'directory created' |
|
549 jid = create_jid() |
|
550 print >>out, '* generated jID automatically:', jid |
|
551 open(os.path.join(path,'package.json'),'w').write(PACKAGE_JSON % {'name':addon.lower(), |
|
552 'title':addon, |
|
553 'id':jid }) |
|
554 print >>out, '* package.json written' |
|
555 open(os.path.join(path,'test','test-main.js'),'w').write(TEST_MAIN_JS) |
|
556 print >>out, '* test/test-main.js written' |
|
557 open(os.path.join(path,'lib','main.js'),'w').write('') |
|
558 print >>out, '* lib/main.js written' |
|
559 if len(args) == 1: |
|
560 print >>out, '\nYour sample add-on is now ready.' |
|
561 print >>out, 'Do "cfx test" to test it and "cfx run" to try it. Have fun!' |
|
562 else: |
|
563 print >>out, '\nYour sample add-on is now ready in the \'' + args[1] + '\' directory.' |
|
564 print >>out, 'Change to that directory, then do "cfx test" to test it, \nand "cfx run" to try it. Have fun!' |
|
565 return {"result":0, "jid":jid} |
|
566 |
|
567 def buildJID(target_cfg): |
|
568 if "id" in target_cfg: |
|
569 jid = target_cfg["id"] |
|
570 else: |
|
571 import uuid |
|
572 jid = str(uuid.uuid4()) |
|
573 if not ("@" in jid or jid.startswith("{")): |
|
574 jid = jid + "@jetpack" |
|
575 return jid |
|
576 |
|
577 def run(arguments=sys.argv[1:], target_cfg=None, pkg_cfg=None, |
|
578 defaults=None, env_root=os.environ.get('CUDDLEFISH_ROOT'), |
|
579 stdout=sys.stdout): |
|
580 versions = get_versions() |
|
581 sdk_version = versions["version"] |
|
582 display_version = "Add-on SDK %s (%s)" % (sdk_version, versions["full"]) |
|
583 parser_kwargs = dict(arguments=arguments, |
|
584 global_options=global_options, |
|
585 parser_groups=parser_groups, |
|
586 usage=usage, |
|
587 version=display_version, |
|
588 defaults=defaults) |
|
589 |
|
590 (options, args) = parse_args(**parser_kwargs) |
|
591 |
|
592 config_args = get_config_args(options.config, env_root); |
|
593 |
|
594 # reparse configs with arguments from local.json |
|
595 if config_args: |
|
596 parser_kwargs['arguments'] += config_args |
|
597 (options, args) = parse_args(**parser_kwargs) |
|
598 |
|
599 command = args[0] |
|
600 |
|
601 if command == "init": |
|
602 initializer(env_root, args) |
|
603 return |
|
604 if command == "testpkgs": |
|
605 test_all_packages(env_root, defaults=options.__dict__) |
|
606 return |
|
607 elif command == "testaddons": |
|
608 test_all_testaddons(env_root, defaults=options.__dict__) |
|
609 return |
|
610 elif command == "testex": |
|
611 test_all_examples(env_root, defaults=options.__dict__) |
|
612 return |
|
613 elif command == "testall": |
|
614 test_all(env_root, defaults=options.__dict__) |
|
615 return |
|
616 elif command == "testcfx": |
|
617 if options.filter: |
|
618 print >>sys.stderr, "The filter option is not valid with the testcfx command" |
|
619 return |
|
620 test_cfx(env_root, options.verbose) |
|
621 return |
|
622 elif command not in ["xpi", "test", "run"]: |
|
623 print >>sys.stderr, "Unknown command: %s" % command |
|
624 print >>sys.stderr, "Try using '--help' for assistance." |
|
625 sys.exit(1) |
|
626 |
|
627 target_cfg_json = None |
|
628 if not target_cfg: |
|
629 if not options.pkgdir: |
|
630 options.pkgdir = find_parent_package(os.getcwd()) |
|
631 if not options.pkgdir: |
|
632 print >>sys.stderr, ("cannot find 'package.json' in the" |
|
633 " current directory or any parent.") |
|
634 sys.exit(1) |
|
635 else: |
|
636 options.pkgdir = os.path.abspath(options.pkgdir) |
|
637 if not os.path.exists(os.path.join(options.pkgdir, 'package.json')): |
|
638 print >>sys.stderr, ("cannot find 'package.json' in" |
|
639 " %s." % options.pkgdir) |
|
640 sys.exit(1) |
|
641 |
|
642 target_cfg_json = os.path.join(options.pkgdir, 'package.json') |
|
643 target_cfg = packaging.get_config_in_dir(options.pkgdir) |
|
644 |
|
645 if options.manifest_overload: |
|
646 for k, v in packaging.load_json_file(options.manifest_overload).items(): |
|
647 target_cfg[k] = v |
|
648 |
|
649 # At this point, we're either building an XPI or running Jetpack code in |
|
650 # a Mozilla application (which includes running tests). |
|
651 |
|
652 use_main = False |
|
653 inherited_options = ['verbose', 'enable_e10s', 'parseable', 'check_memory'] |
|
654 enforce_timeouts = False |
|
655 |
|
656 if command == "xpi": |
|
657 use_main = True |
|
658 elif command == "test": |
|
659 if 'tests' not in target_cfg: |
|
660 target_cfg['tests'] = [] |
|
661 inherited_options.extend(['iterations', 'filter', 'profileMemory', |
|
662 'stopOnError']) |
|
663 enforce_timeouts = True |
|
664 elif command == "run": |
|
665 use_main = True |
|
666 else: |
|
667 assert 0, "shouldn't get here" |
|
668 |
|
669 if use_main and 'main' not in target_cfg: |
|
670 # If the user supplies a template dir, then the main |
|
671 # program may be contained in the template. |
|
672 if not options.templatedir: |
|
673 print >>sys.stderr, "package.json does not have a 'main' entry." |
|
674 sys.exit(1) |
|
675 |
|
676 if not pkg_cfg: |
|
677 pkg_cfg = packaging.build_config(env_root, target_cfg, options.packagepath) |
|
678 |
|
679 target = target_cfg.name |
|
680 |
|
681 # TODO: Consider keeping a cache of dynamic UUIDs, based |
|
682 # on absolute filesystem pathname, in the root directory |
|
683 # or something. |
|
684 if command in ('xpi', 'run'): |
|
685 from cuddlefish.preflight import preflight_config |
|
686 if target_cfg_json: |
|
687 config_was_ok, modified = preflight_config(target_cfg, |
|
688 target_cfg_json) |
|
689 if not config_was_ok: |
|
690 if modified: |
|
691 # we need to re-read package.json . The safest approach |
|
692 # is to re-run the "cfx xpi"/"cfx run" command. |
|
693 print >>sys.stderr, ("package.json modified: please re-run" |
|
694 " 'cfx %s'" % command) |
|
695 else: |
|
696 print >>sys.stderr, ("package.json needs modification:" |
|
697 " please update it and then re-run" |
|
698 " 'cfx %s'" % command) |
|
699 sys.exit(1) |
|
700 # if we make it this far, we have a JID |
|
701 else: |
|
702 assert command == "test" |
|
703 |
|
704 jid = buildJID(target_cfg) |
|
705 |
|
706 targets = [target] |
|
707 if command == "test": |
|
708 targets.append(options.test_runner_pkg) |
|
709 |
|
710 extra_packages = [] |
|
711 if options.extra_packages: |
|
712 extra_packages = options.extra_packages.split(",") |
|
713 if extra_packages: |
|
714 targets.extend(extra_packages) |
|
715 target_cfg.extra_dependencies = extra_packages |
|
716 |
|
717 deps = packaging.get_deps_for_targets(pkg_cfg, targets) |
|
718 |
|
719 from cuddlefish.manifest import build_manifest, ModuleNotFoundError, \ |
|
720 BadChromeMarkerError |
|
721 # Figure out what loader files should be scanned. This is normally |
|
722 # computed inside packaging.generate_build_for_target(), by the first |
|
723 # dependent package that defines a "loader" property in its package.json. |
|
724 # This property is interpreted as a filename relative to the top of that |
|
725 # file, and stored as a path in build.loader . generate_build_for_target() |
|
726 # cannot be called yet (it needs the list of used_deps that |
|
727 # build_manifest() computes, but build_manifest() needs the list of |
|
728 # loader files that it computes). We could duplicate or factor out this |
|
729 # build.loader logic, but that would be messy, so instead we hard-code |
|
730 # the choice of loader for manifest-generation purposes. In practice, |
|
731 # this means that alternative loaders probably won't work with |
|
732 # --strip-xpi. |
|
733 assert packaging.DEFAULT_LOADER == "addon-sdk" |
|
734 assert pkg_cfg.packages["addon-sdk"].loader == "lib/sdk/loader/cuddlefish.js" |
|
735 cuddlefish_js_path = os.path.join(pkg_cfg.packages["addon-sdk"].root_dir, |
|
736 "lib", "sdk", "loader", "cuddlefish.js") |
|
737 loader_modules = [("addon-sdk", "lib", "sdk/loader/cuddlefish", cuddlefish_js_path)] |
|
738 scan_tests = command == "test" |
|
739 test_filter_re = None |
|
740 if scan_tests and options.filter: |
|
741 test_filter_re = options.filter |
|
742 if ":" in options.filter: |
|
743 test_filter_re = options.filter.split(":")[0] |
|
744 try: |
|
745 manifest = build_manifest(target_cfg, pkg_cfg, deps, |
|
746 scan_tests, test_filter_re, |
|
747 loader_modules) |
|
748 except ModuleNotFoundError, e: |
|
749 print str(e) |
|
750 sys.exit(1) |
|
751 except BadChromeMarkerError, e: |
|
752 # An error had already been displayed on stderr in manifest code |
|
753 sys.exit(1) |
|
754 used_deps = manifest.get_used_packages() |
|
755 if command == "test": |
|
756 # The test runner doesn't appear to link against any actual packages, |
|
757 # because it loads everything at runtime (invisible to the linker). |
|
758 # If we believe that, we won't set up URI mappings for anything, and |
|
759 # tests won't be able to run. |
|
760 used_deps = deps |
|
761 for xp in extra_packages: |
|
762 if xp not in used_deps: |
|
763 used_deps.append(xp) |
|
764 |
|
765 build = packaging.generate_build_for_target( |
|
766 pkg_cfg, target, used_deps, |
|
767 include_dep_tests=options.dep_tests, |
|
768 is_running_tests=(command == "test") |
|
769 ) |
|
770 |
|
771 harness_options = { |
|
772 'jetpackID': jid, |
|
773 'staticArgs': options.static_args, |
|
774 'name': target, |
|
775 } |
|
776 |
|
777 harness_options.update(build) |
|
778 |
|
779 # When cfx is run from sdk root directory, we will strip sdk modules and |
|
780 # override them with local modules. |
|
781 # So that integration tools will continue to work and use local modules |
|
782 if os.getcwd() == env_root: |
|
783 options.bundle_sdk = True |
|
784 options.force_use_bundled_sdk = False |
|
785 options.overload_modules = True |
|
786 |
|
787 extra_environment = {} |
|
788 if command == "test": |
|
789 # This should be contained in the test runner package. |
|
790 # maybe just do: target_cfg.main = 'test-harness/run-tests' |
|
791 harness_options['main'] = 'sdk/test/runner' |
|
792 harness_options['mainPath'] = 'sdk/test/runner' |
|
793 else: |
|
794 harness_options['main'] = target_cfg.get('main') |
|
795 harness_options['mainPath'] = manifest.top_path |
|
796 extra_environment["CFX_COMMAND"] = command |
|
797 |
|
798 for option in inherited_options: |
|
799 harness_options[option] = getattr(options, option) |
|
800 |
|
801 harness_options['metadata'] = packaging.get_metadata(pkg_cfg, used_deps) |
|
802 |
|
803 harness_options['sdkVersion'] = sdk_version |
|
804 |
|
805 packaging.call_plugins(pkg_cfg, used_deps) |
|
806 |
|
807 retval = 0 |
|
808 |
|
809 if options.templatedir: |
|
810 app_extension_dir = os.path.abspath(options.templatedir) |
|
811 elif os.path.exists(os.path.join(options.pkgdir, "app-extension")): |
|
812 app_extension_dir = os.path.join(options.pkgdir, "app-extension") |
|
813 else: |
|
814 mydir = os.path.dirname(os.path.abspath(__file__)) |
|
815 app_extension_dir = os.path.join(mydir, "../../app-extension") |
|
816 |
|
817 if target_cfg.get('preferences'): |
|
818 harness_options['preferences'] = target_cfg.get('preferences') |
|
819 |
|
820 # Do not add entries for SDK modules |
|
821 harness_options['manifest'] = manifest.get_harness_options_manifest(False) |
|
822 |
|
823 # Gives an hint to tell if sdk modules are bundled or not |
|
824 harness_options['is-sdk-bundled'] = options.bundle_sdk or options.no_strip_xpi |
|
825 |
|
826 if options.force_use_bundled_sdk: |
|
827 if not harness_options['is-sdk-bundled']: |
|
828 print >>sys.stderr, ("--force-use-bundled-sdk " |
|
829 "can't be used if sdk isn't bundled.") |
|
830 sys.exit(1) |
|
831 if options.overload_modules: |
|
832 print >>sys.stderr, ("--force-use-bundled-sdk and --overload-modules " |
|
833 "can't be used at the same time.") |
|
834 sys.exit(1) |
|
835 # Pass a flag in order to force using sdk modules shipped in the xpi |
|
836 harness_options['force-use-bundled-sdk'] = True |
|
837 |
|
838 # Pass the list of absolute path for all test modules |
|
839 if command == "test": |
|
840 harness_options['allTestModules'] = manifest.get_all_test_modules() |
|
841 if len(harness_options['allTestModules']) == 0: |
|
842 sys.exit(0) |
|
843 |
|
844 from cuddlefish.rdf import gen_manifest, RDFUpdate |
|
845 |
|
846 manifest_rdf = gen_manifest(template_root_dir=app_extension_dir, |
|
847 target_cfg=target_cfg, |
|
848 jid=jid, |
|
849 update_url=options.update_url, |
|
850 bootstrap=True, |
|
851 enable_mobile=options.enable_mobile) |
|
852 |
|
853 if command == "xpi" and options.update_link: |
|
854 if not options.update_link.startswith("https"): |
|
855 raise optparse.OptionValueError("--update-link must start with 'https': %s" % options.update_link) |
|
856 rdf_name = UPDATE_RDF_FILENAME % target_cfg.name |
|
857 print >>stdout, "Exporting update description to %s." % rdf_name |
|
858 update = RDFUpdate() |
|
859 update.add(manifest_rdf, options.update_link) |
|
860 open(rdf_name, "w").write(str(update)) |
|
861 |
|
862 # ask the manifest what files were used, so we can construct an XPI |
|
863 # without the rest. This will include the loader (and everything it |
|
864 # uses) because of the "loader_modules" starting points we passed to |
|
865 # build_manifest earlier |
|
866 used_files = None |
|
867 if command == "xpi": |
|
868 used_files = set(manifest.get_used_files(options.bundle_sdk)) |
|
869 |
|
870 if options.no_strip_xpi: |
|
871 used_files = None # disables the filter, includes all files |
|
872 |
|
873 if command == 'xpi': |
|
874 from cuddlefish.xpi import build_xpi |
|
875 # Generate extra options |
|
876 extra_harness_options = {} |
|
877 for kv in options.extra_harness_option_args: |
|
878 key,value = kv.split("=", 1) |
|
879 extra_harness_options[key] = value |
|
880 # Generate xpi filepath |
|
881 if options.output_file: |
|
882 xpi_path = options.output_file |
|
883 else: |
|
884 xpi_path = XPI_FILENAME % target_cfg.name |
|
885 |
|
886 print >>stdout, "Exporting extension to %s." % xpi_path |
|
887 build_xpi(template_root_dir=app_extension_dir, |
|
888 manifest=manifest_rdf, |
|
889 xpi_path=xpi_path, |
|
890 harness_options=harness_options, |
|
891 limit_to=used_files, |
|
892 extra_harness_options=extra_harness_options, |
|
893 bundle_sdk=True, |
|
894 pkgdir=options.pkgdir) |
|
895 else: |
|
896 from cuddlefish.runner import run_app |
|
897 |
|
898 if options.profiledir: |
|
899 options.profiledir = os.path.expanduser(options.profiledir) |
|
900 options.profiledir = os.path.abspath(options.profiledir) |
|
901 |
|
902 if options.addons is not None: |
|
903 options.addons = options.addons.split(",") |
|
904 |
|
905 try: |
|
906 retval = run_app(harness_root_dir=app_extension_dir, |
|
907 manifest_rdf=manifest_rdf, |
|
908 harness_options=harness_options, |
|
909 app_type=options.app, |
|
910 binary=options.binary, |
|
911 profiledir=options.profiledir, |
|
912 verbose=options.verbose, |
|
913 parseable=options.parseable, |
|
914 enforce_timeouts=enforce_timeouts, |
|
915 logfile=options.logfile, |
|
916 addons=options.addons, |
|
917 args=options.cmdargs, |
|
918 extra_environment=extra_environment, |
|
919 norun=options.no_run, |
|
920 used_files=used_files, |
|
921 enable_mobile=options.enable_mobile, |
|
922 mobile_app_name=options.mobile_app_name, |
|
923 env_root=env_root, |
|
924 is_running_tests=(command == "test"), |
|
925 overload_modules=options.overload_modules, |
|
926 bundle_sdk=options.bundle_sdk, |
|
927 pkgdir=options.pkgdir) |
|
928 except ValueError, e: |
|
929 print "" |
|
930 print "A given cfx option has an inappropriate value:" |
|
931 print >>sys.stderr, " " + " \n ".join(str(e).split("\n")) |
|
932 retval = -1 |
|
933 except Exception, e: |
|
934 if str(e).startswith(MOZRUNNER_BIN_NOT_FOUND): |
|
935 print >>sys.stderr, MOZRUNNER_BIN_NOT_FOUND_HELP.strip() |
|
936 retval = -1 |
|
937 else: |
|
938 raise |
|
939 sys.exit(retval) |