manifest.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  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. import os, sys, re, hashlib
  5. import simplejson as json
  6. SEP = os.path.sep
  7. from cuddlefish.util import filter_filenames, filter_dirnames
  8. # Load new layout mapping hashtable
  9. path = os.path.join(os.environ.get('CUDDLEFISH_ROOT'), "mapping.json")
  10. data = open(path, 'r').read()
  11. NEW_LAYOUT_MAPPING = json.loads(data)
  12. def js_zipname(packagename, modulename):
  13. return "%s-lib/%s.js" % (packagename, modulename)
  14. def docs_zipname(packagename, modulename):
  15. return "%s-docs/%s.md" % (packagename, modulename)
  16. def datamap_zipname(packagename):
  17. return "%s-data.json" % packagename
  18. def datafile_zipname(packagename, datapath):
  19. return "%s-data/%s" % (packagename, datapath)
  20. def to_json(o):
  21. return json.dumps(o, indent=1).encode("utf-8")+"\n"
  22. class ModuleNotFoundError(Exception):
  23. def __init__(self, requirement_type, requirement_name,
  24. used_by, line_number, looked_in):
  25. Exception.__init__(self)
  26. self.requirement_type = requirement_type # "require" or "define"
  27. self.requirement_name = requirement_name # string, what they require()d
  28. self.used_by = used_by # string, full path to module which did require()
  29. self.line_number = line_number # int, 1-indexed line number of first require()
  30. self.looked_in = looked_in # list of full paths to potential .js files
  31. def __str__(self):
  32. what = "%s(%s)" % (self.requirement_type, self.requirement_name)
  33. where = self.used_by
  34. if self.line_number is not None:
  35. where = "%s:%d" % (self.used_by, self.line_number)
  36. searched = "Looked for it in:\n %s\n" % "\n ".join(self.looked_in)
  37. return ("ModuleNotFoundError: unable to satisfy: %s from\n"
  38. " %s:\n" % (what, where)) + searched
  39. class BadModuleIdentifier(Exception):
  40. pass
  41. class BadSection(Exception):
  42. pass
  43. class UnreachablePrefixError(Exception):
  44. pass
  45. class ManifestEntry:
  46. def __init__(self):
  47. self.docs_filename = None
  48. self.docs_hash = None
  49. self.requirements = {}
  50. self.datamap = None
  51. def get_path(self):
  52. name = self.moduleName
  53. if name.endswith(".js"):
  54. name = name[:-3]
  55. items = []
  56. # Only add package name for addons, so that system module paths match
  57. # the path from the commonjs root directory and also match the loader
  58. # mappings.
  59. if self.packageName != "addon-sdk":
  60. items.append(self.packageName)
  61. # And for the same reason, do not append `lib/`.
  62. if self.sectionName == "tests":
  63. items.append(self.sectionName)
  64. items.append(name)
  65. return "/".join(items)
  66. def get_entry_for_manifest(self):
  67. entry = { "packageName": self.packageName,
  68. "sectionName": self.sectionName,
  69. "moduleName": self.moduleName,
  70. "jsSHA256": self.js_hash,
  71. "docsSHA256": self.docs_hash,
  72. "requirements": {},
  73. }
  74. for req in self.requirements:
  75. if isinstance(self.requirements[req], ManifestEntry):
  76. them = self.requirements[req] # this is another ManifestEntry
  77. entry["requirements"][req] = them.get_path()
  78. else:
  79. # something magic. The manifest entry indicates that they're
  80. # allowed to require() it
  81. entry["requirements"][req] = self.requirements[req]
  82. assert isinstance(entry["requirements"][req], unicode) or \
  83. isinstance(entry["requirements"][req], str)
  84. return entry
  85. def add_js(self, js_filename):
  86. self.js_filename = js_filename
  87. self.js_hash = hash_file(js_filename)
  88. def add_docs(self, docs_filename):
  89. self.docs_filename = docs_filename
  90. self.docs_hash = hash_file(docs_filename)
  91. def add_requirement(self, reqname, reqdata):
  92. self.requirements[reqname] = reqdata
  93. def add_data(self, datamap):
  94. self.datamap = datamap
  95. def get_js_zipname(self):
  96. return js_zipname(self.packagename, self.modulename)
  97. def get_docs_zipname(self):
  98. if self.docs_hash:
  99. return docs_zipname(self.packagename, self.modulename)
  100. return None
  101. # self.js_filename
  102. # self.docs_filename
  103. def hash_file(fn):
  104. return hashlib.sha256(open(fn,"rb").read()).hexdigest()
  105. def get_datafiles(datadir):
  106. # yields pathnames relative to DATADIR, ignoring some files
  107. for dirpath, dirnames, filenames in os.walk(datadir):
  108. filenames = list(filter_filenames(filenames))
  109. # this tells os.walk to prune the search
  110. dirnames[:] = filter_dirnames(dirnames)
  111. for filename in filenames:
  112. fullname = os.path.join(dirpath, filename)
  113. assert fullname.startswith(datadir+SEP), "%s%s not in %s" % (datadir, SEP, fullname)
  114. yield fullname[len(datadir+SEP):]
  115. class DataMap:
  116. # one per package
  117. def __init__(self, pkg):
  118. self.pkg = pkg
  119. self.name = pkg.name
  120. self.files_to_copy = []
  121. datamap = {}
  122. datadir = os.path.join(pkg.root_dir, "data")
  123. for dataname in get_datafiles(datadir):
  124. absname = os.path.join(datadir, dataname)
  125. zipname = datafile_zipname(pkg.name, dataname)
  126. datamap[dataname] = hash_file(absname)
  127. self.files_to_copy.append( (zipname, absname) )
  128. self.data_manifest = to_json(datamap)
  129. self.data_manifest_hash = hashlib.sha256(self.data_manifest).hexdigest()
  130. self.data_manifest_zipname = datamap_zipname(pkg.name)
  131. self.data_uri_prefix = "%s/data/" % (self.name)
  132. class BadChromeMarkerError(Exception):
  133. pass
  134. class ModuleInfo:
  135. def __init__(self, package, section, name, js, docs):
  136. self.package = package
  137. self.section = section
  138. self.name = name
  139. self.js = js
  140. self.docs = docs
  141. def __hash__(self):
  142. return hash( (self.package.name, self.section, self.name,
  143. self.js, self.docs) )
  144. def __eq__(self, them):
  145. if them.__class__ is not self.__class__:
  146. return False
  147. if ((them.package.name, them.section, them.name, them.js, them.docs) !=
  148. (self.package.name, self.section, self.name, self.js, self.docs) ):
  149. return False
  150. return True
  151. def __repr__(self):
  152. return "ModuleInfo [%s %s %s] (%s, %s)" % (self.package.name,
  153. self.section,
  154. self.name,
  155. self.js, self.docs)
  156. class ManifestBuilder:
  157. def __init__(self, target_cfg, pkg_cfg, deps, extra_modules,
  158. stderr=sys.stderr):
  159. self.manifest = {} # maps (package,section,module) to ManifestEntry
  160. self.target_cfg = target_cfg # the entry point
  161. self.pkg_cfg = pkg_cfg # all known packages
  162. self.deps = deps # list of package names to search
  163. self.used_packagenames = set()
  164. self.stderr = stderr
  165. self.extra_modules = extra_modules
  166. self.modules = {} # maps ModuleInfo to URI in self.manifest
  167. self.datamaps = {} # maps package name to DataMap instance
  168. self.files = [] # maps manifest index to (absfn,absfn) js/docs pair
  169. self.test_modules = [] # for runtime
  170. def build(self, scan_tests, test_filter_re):
  171. # process the top module, which recurses to process everything it
  172. # reaches
  173. if "main" in self.target_cfg:
  174. top_mi = self.find_top(self.target_cfg)
  175. top_me = self.process_module(top_mi)
  176. self.top_path = top_me.get_path()
  177. self.datamaps[self.target_cfg.name] = DataMap(self.target_cfg)
  178. if scan_tests:
  179. mi = self._find_module_in_package("addon-sdk", "lib", "sdk/test/runner", [])
  180. self.process_module(mi)
  181. # also scan all test files in all packages that we use. By making
  182. # a copy of self.used_packagenames first, we refrain from
  183. # processing tests in packages that our own tests depend upon. If
  184. # we're running tests for package A, and either modules in A or
  185. # tests in A depend upon modules from package B, we *don't* want
  186. # to run tests for package B.
  187. test_modules = []
  188. dirnames = self.target_cfg["tests"]
  189. if isinstance(dirnames, basestring):
  190. dirnames = [dirnames]
  191. dirnames = [os.path.join(self.target_cfg.root_dir, d)
  192. for d in dirnames]
  193. for d in dirnames:
  194. for filename in os.listdir(d):
  195. if filename.startswith("test-") and filename.endswith(".js"):
  196. testname = filename[:-3] # require(testname)
  197. if test_filter_re:
  198. if not re.search(test_filter_re, testname):
  199. continue
  200. tmi = ModuleInfo(self.target_cfg, "tests", testname,
  201. os.path.join(d, filename), None)
  202. # scan the test's dependencies
  203. tme = self.process_module(tmi)
  204. test_modules.append( (testname, tme) )
  205. # also add it as an artificial dependency of unit-test-finder, so
  206. # the runtime dynamic load can work.
  207. test_finder = self.get_manifest_entry("addon-sdk", "lib",
  208. "sdk/deprecated/unit-test-finder")
  209. for (testname,tme) in test_modules:
  210. test_finder.add_requirement(testname, tme)
  211. # finally, tell the runtime about it, so they won't have to
  212. # search for all tests. self.test_modules will be passed
  213. # through the harness-options.json file in the
  214. # .allTestModules property.
  215. # Pass the absolute module path.
  216. self.test_modules.append(tme.get_path())
  217. # include files used by the loader
  218. for em in self.extra_modules:
  219. (pkgname, section, modname, js) = em
  220. mi = ModuleInfo(self.pkg_cfg.packages[pkgname], section, modname,
  221. js, None)
  222. self.process_module(mi)
  223. def get_module_entries(self):
  224. return frozenset(self.manifest.values())
  225. def get_data_entries(self):
  226. return frozenset(self.datamaps.values())
  227. def get_used_packages(self):
  228. used = set()
  229. for index in self.manifest:
  230. (package, section, module) = index
  231. used.add(package)
  232. return sorted(used)
  233. def get_used_files(self, bundle_sdk_modules):
  234. # returns all .js files that we reference, plus data/ files. You will
  235. # need to add the loader, off-manifest files that it needs, and
  236. # generated metadata.
  237. for datamap in self.datamaps.values():
  238. for (zipname, absname) in datamap.files_to_copy:
  239. yield absname
  240. for me in self.get_module_entries():
  241. # Only ship SDK files if we are told to do so
  242. if me.packageName != "addon-sdk" or bundle_sdk_modules:
  243. yield me.js_filename
  244. def get_all_test_modules(self):
  245. return self.test_modules
  246. def get_harness_options_manifest(self, bundle_sdk_modules):
  247. manifest = {}
  248. for me in self.get_module_entries():
  249. path = me.get_path()
  250. # Do not add manifest entries for system modules.
  251. # Doesn't prevent from shipping modules.
  252. # Shipping modules is decided in `get_used_files`.
  253. if me.packageName != "addon-sdk" or bundle_sdk_modules:
  254. manifest[path] = me.get_entry_for_manifest()
  255. return manifest
  256. def get_manifest_entry(self, package, section, module):
  257. index = (package, section, module)
  258. if index not in self.manifest:
  259. m = self.manifest[index] = ManifestEntry()
  260. m.packageName = package
  261. m.sectionName = section
  262. m.moduleName = module
  263. self.used_packagenames.add(package)
  264. return self.manifest[index]
  265. def uri_name_from_path(self, pkg, fn):
  266. # given a filename like .../pkg1/lib/bar/foo.js, and a package
  267. # specification (with a .root_dir like ".../pkg1" and a .lib list of
  268. # paths where .lib[0] is like "lib"), return the appropriate NAME
  269. # that can be put into a URI like resource://JID-pkg1-lib/NAME . This
  270. # will throw an exception if the file is outside of the lib/
  271. # directory, since that means we can't construct a URI that points to
  272. # it.
  273. #
  274. # This should be a lot easier, and shouldn't fail when the file is in
  275. # the root of the package. Both should become possible when the XPI
  276. # is rearranged and our URI scheme is simplified.
  277. fn = os.path.abspath(fn)
  278. pkglib = pkg.lib[0]
  279. libdir = os.path.abspath(os.path.join(pkg.root_dir, pkglib))
  280. # AARGH, section and name! we need to reverse-engineer a
  281. # ModuleInfo instance that will produce a URI (in the form
  282. # PREFIX/PKGNAME-SECTION/JS) that will map to the existing file.
  283. # Until we fix URI generation to get rid of "sections", this is
  284. # limited to files in the same .directories.lib as the rest of
  285. # the package uses. So if the package's main files are in lib/,
  286. # but the main.js is in the package root, there is no URI we can
  287. # construct that will point to it, and we must fail.
  288. #
  289. # This will become much easier (and the failure case removed)
  290. # when we get rid of sections and change the URIs to look like
  291. # (PREFIX/PKGNAME/PATH-TO-JS).
  292. # AARGH 2, allowing .lib to be a list is really getting in the
  293. # way. That needs to go away eventually too.
  294. if not fn.startswith(libdir):
  295. raise UnreachablePrefixError("Sorry, but the 'main' file (%s) in package %s is outside that package's 'lib' directory (%s), so I cannot construct a URI to reach it."
  296. % (fn, pkg.name, pkglib))
  297. name = fn[len(libdir):].lstrip(SEP)[:-len(".js")]
  298. return name
  299. def parse_main(self, root_dir, main, check_lib_dir=None):
  300. # 'main' can be like one of the following:
  301. # a: ./lib/main.js b: ./lib/main c: lib/main
  302. # we require it to be a path to the file, though, and ignore the
  303. # .directories stuff. So just "main" is insufficient if you really
  304. # want something in a "lib/" subdirectory.
  305. if main.endswith(".js"):
  306. main = main[:-len(".js")]
  307. if main.startswith("./"):
  308. main = main[len("./"):]
  309. # package.json must always use "/", but on windows we'll replace that
  310. # with "\" before using it as an actual filename
  311. main = os.sep.join(main.split("/"))
  312. paths = [os.path.join(root_dir, main+".js")]
  313. if check_lib_dir is not None:
  314. paths.append(os.path.join(root_dir, check_lib_dir, main+".js"))
  315. return paths
  316. def find_top_js(self, target_cfg):
  317. for libdir in target_cfg.lib:
  318. for n in self.parse_main(target_cfg.root_dir, target_cfg.main,
  319. libdir):
  320. if os.path.exists(n):
  321. return n
  322. raise KeyError("unable to find main module '%s.js' in top-level package" % target_cfg.main)
  323. def find_top(self, target_cfg):
  324. top_js = self.find_top_js(target_cfg)
  325. n = os.path.join(target_cfg.root_dir, "README.md")
  326. if os.path.exists(n):
  327. top_docs = n
  328. else:
  329. top_docs = None
  330. name = self.uri_name_from_path(target_cfg, top_js)
  331. return ModuleInfo(target_cfg, "lib", name, top_js, top_docs)
  332. def process_module(self, mi):
  333. pkg = mi.package
  334. #print "ENTERING", pkg.name, mi.name
  335. # mi.name must be fully-qualified
  336. assert (not mi.name.startswith("./") and
  337. not mi.name.startswith("../"))
  338. # create and claim the manifest row first
  339. me = self.get_manifest_entry(pkg.name, mi.section, mi.name)
  340. me.add_js(mi.js)
  341. if mi.docs:
  342. me.add_docs(mi.docs)
  343. js_lines = open(mi.js,"r").readlines()
  344. requires, problems, locations = scan_module(mi.js,js_lines,self.stderr)
  345. if problems:
  346. # the relevant instructions have already been written to stderr
  347. raise BadChromeMarkerError()
  348. # We update our requirements on the way out of the depth-first
  349. # traversal of the module graph
  350. for reqname in sorted(requires.keys()):
  351. # If requirement is chrome or a pseudo-module (starts with @) make
  352. # path a requirement name.
  353. if reqname == "chrome" or reqname.startswith("@"):
  354. me.add_requirement(reqname, reqname)
  355. else:
  356. # when two modules require() the same name, do they get a
  357. # shared instance? This is a deep question. For now say yes.
  358. # find_req_for() returns an entry to put in our
  359. # 'requirements' dict, and will recursively process
  360. # everything transitively required from here. It will also
  361. # populate the self.modules[] cache. Note that we must
  362. # tolerate cycles in the reference graph.
  363. looked_in = [] # populated by subroutines
  364. them_me = self.find_req_for(mi, reqname, looked_in, locations)
  365. if them_me is None:
  366. if mi.section == "tests":
  367. # tolerate missing modules in tests, because
  368. # test-securable-module.js, and the modules/red.js
  369. # that it imports, both do that intentionally
  370. continue
  371. lineno = locations.get(reqname) # None means define()
  372. if lineno is None:
  373. reqtype = "define"
  374. else:
  375. reqtype = "require"
  376. err = ModuleNotFoundError(reqtype, reqname,
  377. mi.js, lineno, looked_in)
  378. raise err
  379. else:
  380. me.add_requirement(reqname, them_me)
  381. return me
  382. #print "LEAVING", pkg.name, mi.name
  383. def find_req_for(self, from_module, reqname, looked_in, locations):
  384. # handle a single require(reqname) statement from from_module .
  385. # Return a uri that exists in self.manifest
  386. # Populate looked_in with places we looked.
  387. def BAD(msg):
  388. return BadModuleIdentifier(msg + " in require(%s) from %s" %
  389. (reqname, from_module))
  390. if not reqname:
  391. raise BAD("no actual modulename")
  392. # Allow things in tests/*.js to require both test code and real code.
  393. # But things in lib/*.js can only require real code.
  394. if from_module.section == "tests":
  395. lookfor_sections = ["tests", "lib"]
  396. elif from_module.section == "lib":
  397. lookfor_sections = ["lib"]
  398. else:
  399. raise BadSection(from_module.section)
  400. modulename = from_module.name
  401. #print " %s require(%s))" % (from_module, reqname)
  402. if reqname.startswith("./") or reqname.startswith("../"):
  403. # 1: they want something relative to themselves, always from
  404. # their own package
  405. them = modulename.split("/")[:-1]
  406. bits = reqname.split("/")
  407. while bits[0] in (".", ".."):
  408. if not bits:
  409. raise BAD("no actual modulename")
  410. if bits[0] == "..":
  411. if not them:
  412. raise BAD("too many ..")
  413. them.pop()
  414. bits.pop(0)
  415. bits = them+bits
  416. lookfor_pkg = from_module.package.name
  417. lookfor_mod = "/".join(bits)
  418. return self._get_module_from_package(lookfor_pkg,
  419. lookfor_sections, lookfor_mod,
  420. looked_in)
  421. # non-relative import. Might be a short name (requiring a search
  422. # through "library" packages), or a fully-qualified one.
  423. if "/" in reqname:
  424. # 2: PKG/MOD: find PKG, look inside for MOD
  425. bits = reqname.split("/")
  426. lookfor_pkg = bits[0]
  427. lookfor_mod = "/".join(bits[1:])
  428. mi = self._get_module_from_package(lookfor_pkg,
  429. lookfor_sections, lookfor_mod,
  430. looked_in)
  431. if mi: # caution, 0==None
  432. return mi
  433. else:
  434. # 3: try finding PKG, if found, use its main.js entry point
  435. lookfor_pkg = reqname
  436. mi = self._get_entrypoint_from_package(lookfor_pkg, looked_in)
  437. if mi:
  438. return mi
  439. # 4: search packages for MOD or MODPARENT/MODCHILD. We always search
  440. # their own package first, then the list of packages defined by their
  441. # .dependencies list
  442. from_pkg = from_module.package.name
  443. mi = self._search_packages_for_module(from_pkg,
  444. lookfor_sections, reqname,
  445. looked_in)
  446. if mi:
  447. return mi
  448. # Only after we look for module in the addon itself, search for a module
  449. # in new layout.
  450. # First normalize require argument in order to easily find a mapping
  451. normalized = reqname
  452. if normalized.endswith(".js"):
  453. normalized = normalized[:-len(".js")]
  454. if normalized.startswith("addon-kit/"):
  455. normalized = normalized[len("addon-kit/"):]
  456. if normalized.startswith("api-utils/"):
  457. normalized = normalized[len("api-utils/"):]
  458. if normalized in NEW_LAYOUT_MAPPING:
  459. # get the new absolute path for this module
  460. original_reqname = reqname
  461. reqname = NEW_LAYOUT_MAPPING[normalized]
  462. from_pkg = from_module.package.name
  463. # If the addon didn't explicitely told us to ignore deprecated
  464. # require path, warn the developer:
  465. # (target_cfg is the package.json file)
  466. if not "ignore-deprecated-path" in self.target_cfg:
  467. lineno = locations.get(original_reqname)
  468. print >>self.stderr, "Warning: Use of deprecated require path:"
  469. print >>self.stderr, " In %s:%d:" % (from_module.js, lineno)
  470. print >>self.stderr, " require('%s')." % original_reqname
  471. print >>self.stderr, " New path should be:"
  472. print >>self.stderr, " require('%s')" % reqname
  473. return self._search_packages_for_module(from_pkg,
  474. lookfor_sections, reqname,
  475. looked_in)
  476. else:
  477. # We weren't able to find this module, really.
  478. return None
  479. def _handle_module(self, mi):
  480. if not mi:
  481. return None
  482. # we tolerate cycles in the reference graph, which means we need to
  483. # populate the self.modules cache before recursing into
  484. # process_module() . We must also check the cache first, so recursion
  485. # can terminate.
  486. if mi in self.modules:
  487. return self.modules[mi]
  488. # this creates the entry
  489. new_entry = self.get_manifest_entry(mi.package.name, mi.section, mi.name)
  490. # and populates the cache
  491. self.modules[mi] = new_entry
  492. self.process_module(mi)
  493. return new_entry
  494. def _get_module_from_package(self, pkgname, sections, modname, looked_in):
  495. if pkgname not in self.pkg_cfg.packages:
  496. return None
  497. mi = self._find_module_in_package(pkgname, sections, modname,
  498. looked_in)
  499. return self._handle_module(mi)
  500. def _get_entrypoint_from_package(self, pkgname, looked_in):
  501. if pkgname not in self.pkg_cfg.packages:
  502. return None
  503. pkg = self.pkg_cfg.packages[pkgname]
  504. main = pkg.get("main", None)
  505. if not main:
  506. return None
  507. for js in self.parse_main(pkg.root_dir, main):
  508. looked_in.append(js)
  509. if os.path.exists(js):
  510. section = "lib"
  511. name = self.uri_name_from_path(pkg, js)
  512. docs = None
  513. mi = ModuleInfo(pkg, section, name, js, docs)
  514. return self._handle_module(mi)
  515. return None
  516. def _search_packages_for_module(self, from_pkg, sections, reqname,
  517. looked_in):
  518. searchpath = [] # list of package names
  519. searchpath.append(from_pkg) # search self first
  520. us = self.pkg_cfg.packages[from_pkg]
  521. if 'dependencies' in us:
  522. # only look in dependencies
  523. searchpath.extend(us['dependencies'])
  524. else:
  525. # they didn't declare any dependencies (or they declared an empty
  526. # list, but we'll treat that as not declaring one, because it's
  527. # easier), so look in all deps, sorted alphabetically, so
  528. # addon-kit comes first. Note that self.deps includes all
  529. # packages found by traversing the ".dependencies" lists in each
  530. # package.json, starting from the main addon package, plus
  531. # everything added by --extra-packages
  532. searchpath.extend(sorted(self.deps))
  533. for pkgname in searchpath:
  534. mi = self._find_module_in_package(pkgname, sections, reqname,
  535. looked_in)
  536. if mi:
  537. return self._handle_module(mi)
  538. return None
  539. def _find_module_in_package(self, pkgname, sections, name, looked_in):
  540. # require("a/b/c") should look at ...\a\b\c.js on windows
  541. filename = os.sep.join(name.split("/"))
  542. # normalize filename, make sure that we do not add .js if it already has
  543. # it.
  544. if not filename.endswith(".js") and not filename.endswith(".json"):
  545. filename += ".js"
  546. if filename.endswith(".js"):
  547. basename = filename[:-3]
  548. if filename.endswith(".json"):
  549. basename = filename[:-5]
  550. pkg = self.pkg_cfg.packages[pkgname]
  551. if isinstance(sections, basestring):
  552. sections = [sections]
  553. for section in sections:
  554. for sdir in pkg.get(section, []):
  555. js = os.path.join(pkg.root_dir, sdir, filename)
  556. looked_in.append(js)
  557. if os.path.exists(js):
  558. docs = None
  559. maybe_docs = os.path.join(pkg.root_dir, "docs",
  560. basename+".md")
  561. if section == "lib" and os.path.exists(maybe_docs):
  562. docs = maybe_docs
  563. return ModuleInfo(pkg, section, name, js, docs)
  564. return None
  565. def build_manifest(target_cfg, pkg_cfg, deps, scan_tests,
  566. test_filter_re=None, extra_modules=[]):
  567. """
  568. Perform recursive dependency analysis starting from entry_point,
  569. building up a manifest of modules that need to be included in the XPI.
  570. Each entry will map require() names to the URL of the module that will
  571. be used to satisfy that dependency. The manifest will be used by the
  572. runtime's require() code.
  573. This returns a ManifestBuilder object, with two public methods. The
  574. first, get_module_entries(), returns a set of ManifestEntry objects, each
  575. of which can be asked for the following:
  576. * its contribution to the harness-options.json '.manifest'
  577. * the local disk name
  578. * the name in the XPI at which it should be placed
  579. The second is get_data_entries(), which returns a set of DataEntry
  580. objects, each of which has:
  581. * local disk name
  582. * name in the XPI
  583. note: we don't build the XPI here, but our manifest is passed to the
  584. code which does, so it knows what to copy into the XPI.
  585. """
  586. mxt = ManifestBuilder(target_cfg, pkg_cfg, deps, extra_modules)
  587. mxt.build(scan_tests, test_filter_re)
  588. return mxt
  589. COMMENT_PREFIXES = ["//", "/*", "*", "dump("]
  590. REQUIRE_RE = r"(?<![\'\"])require\s*\(\s*[\'\"]([^\'\"]+?)[\'\"]\s*\)"
  591. # detect the define idiom of the form:
  592. # define("module name", ["dep1", "dep2", "dep3"], function() {})
  593. # by capturing the contents of the list in a group.
  594. DEF_RE = re.compile(r"(require|define)\s*\(\s*([\'\"][^\'\"]+[\'\"]\s*,)?\s*\[([^\]]+)\]")
  595. # Out of the async dependencies, do not allow quotes in them.
  596. DEF_RE_ALLOWED = re.compile(r"^[\'\"][^\'\"]+[\'\"]$")
  597. def scan_requirements_with_grep(fn, lines):
  598. requires = {}
  599. first_location = {}
  600. for (lineno0, line) in enumerate(lines):
  601. for clause in line.split(";"):
  602. clause = clause.strip()
  603. iscomment = False
  604. for commentprefix in COMMENT_PREFIXES:
  605. if clause.startswith(commentprefix):
  606. iscomment = True
  607. if iscomment:
  608. continue
  609. mo = re.finditer(REQUIRE_RE, clause)
  610. if mo:
  611. for mod in mo:
  612. modname = mod.group(1)
  613. requires[modname] = {}
  614. if modname not in first_location:
  615. first_location[modname] = lineno0 + 1
  616. # define() can happen across multiple lines, so join everyone up.
  617. wholeshebang = "\n".join(lines)
  618. for match in DEF_RE.finditer(wholeshebang):
  619. # this should net us a list of string literals separated by commas
  620. for strbit in match.group(3).split(","):
  621. strbit = strbit.strip()
  622. # There could be a trailing comma netting us just whitespace, so
  623. # filter that out. Make sure that only string values with
  624. # quotes around them are allowed, and no quotes are inside
  625. # the quoted value.
  626. if strbit and DEF_RE_ALLOWED.match(strbit):
  627. modname = strbit[1:-1]
  628. if modname not in ["exports"]:
  629. requires[modname] = {}
  630. # joining all the lines means we lose line numbers, so we
  631. # can't fill first_location[]
  632. return requires, first_location
  633. CHROME_ALIASES = [
  634. (re.compile(r"Components\.classes"), "Cc"),
  635. (re.compile(r"Components\.interfaces"), "Ci"),
  636. (re.compile(r"Components\.utils"), "Cu"),
  637. (re.compile(r"Components\.results"), "Cr"),
  638. (re.compile(r"Components\.manager"), "Cm"),
  639. ]
  640. OTHER_CHROME = re.compile(r"Components\.[a-zA-Z]")
  641. def scan_for_bad_chrome(fn, lines, stderr):
  642. problems = False
  643. old_chrome = set() # i.e. "Cc" when we see "Components.classes"
  644. old_chrome_lines = [] # list of (lineno, line.strip()) tuples
  645. for lineno,line in enumerate(lines):
  646. # note: this scanner is not obligated to spot all possible forms of
  647. # chrome access. The scanner is detecting voluntary requests for
  648. # chrome. Runtime tools will enforce allowance or denial of access.
  649. line = line.strip()
  650. iscomment = False
  651. for commentprefix in COMMENT_PREFIXES:
  652. if line.startswith(commentprefix):
  653. iscomment = True
  654. break
  655. if iscomment:
  656. continue
  657. old_chrome_in_this_line = set()
  658. for (regexp,alias) in CHROME_ALIASES:
  659. if regexp.search(line):
  660. old_chrome_in_this_line.add(alias)
  661. if not old_chrome_in_this_line:
  662. if OTHER_CHROME.search(line):
  663. old_chrome_in_this_line.add("components")
  664. old_chrome.update(old_chrome_in_this_line)
  665. if old_chrome_in_this_line:
  666. old_chrome_lines.append( (lineno+1, line) )
  667. if old_chrome:
  668. print >>stderr, """
  669. The following lines from file %(fn)s:
  670. %(lines)s
  671. use 'Components' to access chrome authority. To do so, you need to add a
  672. line somewhat like the following:
  673. const {%(needs)s} = require("chrome");
  674. Then you can use any shortcuts to its properties that you import from the
  675. 'chrome' module ('Cc', 'Ci', 'Cm', 'Cr', and 'Cu' for the 'classes',
  676. 'interfaces', 'manager', 'results', and 'utils' properties, respectively. And
  677. `components` for `Components` object itself).
  678. """ % { "fn": fn, "needs": ",".join(sorted(old_chrome)),
  679. "lines": "\n".join([" %3d: %s" % (lineno,line)
  680. for (lineno, line) in old_chrome_lines]),
  681. }
  682. problems = True
  683. return problems
  684. def scan_module(fn, lines, stderr=sys.stderr):
  685. filename = os.path.basename(fn)
  686. requires, locations = scan_requirements_with_grep(fn, lines)
  687. if filename == "cuddlefish.js":
  688. # this is the loader: don't scan for chrome
  689. problems = False
  690. else:
  691. problems = scan_for_bad_chrome(fn, lines, stderr)
  692. return requires, problems, locations
  693. if __name__ == '__main__':
  694. for fn in sys.argv[1:]:
  695. requires, problems, locations = scan_module(fn, open(fn).readlines())
  696. print
  697. print "---", fn
  698. if problems:
  699. print "PROBLEMS"
  700. sys.exit(1)
  701. print "requires: %s" % (",".join(sorted(requires.keys())))
  702. print "locations: %s" % locations