1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/python-lib/cuddlefish/manifest.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,795 @@ 1.4 +# This Source Code Form is subject to the terms of the Mozilla Public 1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 + 1.9 +import os, sys, re, hashlib 1.10 +import simplejson as json 1.11 +SEP = os.path.sep 1.12 +from cuddlefish.util import filter_filenames, filter_dirnames 1.13 + 1.14 +# Load new layout mapping hashtable 1.15 +path = os.path.join(os.environ.get('CUDDLEFISH_ROOT'), "mapping.json") 1.16 +data = open(path, 'r').read() 1.17 +NEW_LAYOUT_MAPPING = json.loads(data) 1.18 + 1.19 +def js_zipname(packagename, modulename): 1.20 + return "%s-lib/%s.js" % (packagename, modulename) 1.21 +def docs_zipname(packagename, modulename): 1.22 + return "%s-docs/%s.md" % (packagename, modulename) 1.23 +def datamap_zipname(packagename): 1.24 + return "%s-data.json" % packagename 1.25 +def datafile_zipname(packagename, datapath): 1.26 + return "%s-data/%s" % (packagename, datapath) 1.27 + 1.28 +def to_json(o): 1.29 + return json.dumps(o, indent=1).encode("utf-8")+"\n" 1.30 + 1.31 +class ModuleNotFoundError(Exception): 1.32 + def __init__(self, requirement_type, requirement_name, 1.33 + used_by, line_number, looked_in): 1.34 + Exception.__init__(self) 1.35 + self.requirement_type = requirement_type # "require" or "define" 1.36 + self.requirement_name = requirement_name # string, what they require()d 1.37 + self.used_by = used_by # string, full path to module which did require() 1.38 + self.line_number = line_number # int, 1-indexed line number of first require() 1.39 + self.looked_in = looked_in # list of full paths to potential .js files 1.40 + def __str__(self): 1.41 + what = "%s(%s)" % (self.requirement_type, self.requirement_name) 1.42 + where = self.used_by 1.43 + if self.line_number is not None: 1.44 + where = "%s:%d" % (self.used_by, self.line_number) 1.45 + searched = "Looked for it in:\n %s\n" % "\n ".join(self.looked_in) 1.46 + return ("ModuleNotFoundError: unable to satisfy: %s from\n" 1.47 + " %s:\n" % (what, where)) + searched 1.48 + 1.49 +class BadModuleIdentifier(Exception): 1.50 + pass 1.51 +class BadSection(Exception): 1.52 + pass 1.53 +class UnreachablePrefixError(Exception): 1.54 + pass 1.55 + 1.56 +class ManifestEntry: 1.57 + def __init__(self): 1.58 + self.docs_filename = None 1.59 + self.docs_hash = None 1.60 + self.requirements = {} 1.61 + self.datamap = None 1.62 + 1.63 + def get_path(self): 1.64 + name = self.moduleName 1.65 + 1.66 + if name.endswith(".js"): 1.67 + name = name[:-3] 1.68 + items = [] 1.69 + # Only add package name for addons, so that system module paths match 1.70 + # the path from the commonjs root directory and also match the loader 1.71 + # mappings. 1.72 + if self.packageName != "addon-sdk": 1.73 + items.append(self.packageName) 1.74 + # And for the same reason, do not append `lib/`. 1.75 + if self.sectionName == "tests": 1.76 + items.append(self.sectionName) 1.77 + items.append(name) 1.78 + 1.79 + return "/".join(items) 1.80 + 1.81 + def get_entry_for_manifest(self): 1.82 + entry = { "packageName": self.packageName, 1.83 + "sectionName": self.sectionName, 1.84 + "moduleName": self.moduleName, 1.85 + "jsSHA256": self.js_hash, 1.86 + "docsSHA256": self.docs_hash, 1.87 + "requirements": {}, 1.88 + } 1.89 + for req in self.requirements: 1.90 + if isinstance(self.requirements[req], ManifestEntry): 1.91 + them = self.requirements[req] # this is another ManifestEntry 1.92 + entry["requirements"][req] = them.get_path() 1.93 + else: 1.94 + # something magic. The manifest entry indicates that they're 1.95 + # allowed to require() it 1.96 + entry["requirements"][req] = self.requirements[req] 1.97 + assert isinstance(entry["requirements"][req], unicode) or \ 1.98 + isinstance(entry["requirements"][req], str) 1.99 + return entry 1.100 + 1.101 + def add_js(self, js_filename): 1.102 + self.js_filename = js_filename 1.103 + self.js_hash = hash_file(js_filename) 1.104 + def add_docs(self, docs_filename): 1.105 + self.docs_filename = docs_filename 1.106 + self.docs_hash = hash_file(docs_filename) 1.107 + def add_requirement(self, reqname, reqdata): 1.108 + self.requirements[reqname] = reqdata 1.109 + def add_data(self, datamap): 1.110 + self.datamap = datamap 1.111 + 1.112 + def get_js_zipname(self): 1.113 + return js_zipname(self.packagename, self.modulename) 1.114 + def get_docs_zipname(self): 1.115 + if self.docs_hash: 1.116 + return docs_zipname(self.packagename, self.modulename) 1.117 + return None 1.118 + # self.js_filename 1.119 + # self.docs_filename 1.120 + 1.121 + 1.122 +def hash_file(fn): 1.123 + return hashlib.sha256(open(fn,"rb").read()).hexdigest() 1.124 + 1.125 +def get_datafiles(datadir): 1.126 + # yields pathnames relative to DATADIR, ignoring some files 1.127 + for dirpath, dirnames, filenames in os.walk(datadir): 1.128 + filenames = list(filter_filenames(filenames)) 1.129 + # this tells os.walk to prune the search 1.130 + dirnames[:] = filter_dirnames(dirnames) 1.131 + for filename in filenames: 1.132 + fullname = os.path.join(dirpath, filename) 1.133 + assert fullname.startswith(datadir+SEP), "%s%s not in %s" % (datadir, SEP, fullname) 1.134 + yield fullname[len(datadir+SEP):] 1.135 + 1.136 + 1.137 +class DataMap: 1.138 + # one per package 1.139 + def __init__(self, pkg): 1.140 + self.pkg = pkg 1.141 + self.name = pkg.name 1.142 + self.files_to_copy = [] 1.143 + datamap = {} 1.144 + datadir = os.path.join(pkg.root_dir, "data") 1.145 + for dataname in get_datafiles(datadir): 1.146 + absname = os.path.join(datadir, dataname) 1.147 + zipname = datafile_zipname(pkg.name, dataname) 1.148 + datamap[dataname] = hash_file(absname) 1.149 + self.files_to_copy.append( (zipname, absname) ) 1.150 + self.data_manifest = to_json(datamap) 1.151 + self.data_manifest_hash = hashlib.sha256(self.data_manifest).hexdigest() 1.152 + self.data_manifest_zipname = datamap_zipname(pkg.name) 1.153 + self.data_uri_prefix = "%s/data/" % (self.name) 1.154 + 1.155 +class BadChromeMarkerError(Exception): 1.156 + pass 1.157 + 1.158 +class ModuleInfo: 1.159 + def __init__(self, package, section, name, js, docs): 1.160 + self.package = package 1.161 + self.section = section 1.162 + self.name = name 1.163 + self.js = js 1.164 + self.docs = docs 1.165 + 1.166 + def __hash__(self): 1.167 + return hash( (self.package.name, self.section, self.name, 1.168 + self.js, self.docs) ) 1.169 + def __eq__(self, them): 1.170 + if them.__class__ is not self.__class__: 1.171 + return False 1.172 + if ((them.package.name, them.section, them.name, them.js, them.docs) != 1.173 + (self.package.name, self.section, self.name, self.js, self.docs) ): 1.174 + return False 1.175 + return True 1.176 + 1.177 + def __repr__(self): 1.178 + return "ModuleInfo [%s %s %s] (%s, %s)" % (self.package.name, 1.179 + self.section, 1.180 + self.name, 1.181 + self.js, self.docs) 1.182 + 1.183 +class ManifestBuilder: 1.184 + def __init__(self, target_cfg, pkg_cfg, deps, extra_modules, 1.185 + stderr=sys.stderr): 1.186 + self.manifest = {} # maps (package,section,module) to ManifestEntry 1.187 + self.target_cfg = target_cfg # the entry point 1.188 + self.pkg_cfg = pkg_cfg # all known packages 1.189 + self.deps = deps # list of package names to search 1.190 + self.used_packagenames = set() 1.191 + self.stderr = stderr 1.192 + self.extra_modules = extra_modules 1.193 + self.modules = {} # maps ModuleInfo to URI in self.manifest 1.194 + self.datamaps = {} # maps package name to DataMap instance 1.195 + self.files = [] # maps manifest index to (absfn,absfn) js/docs pair 1.196 + self.test_modules = [] # for runtime 1.197 + 1.198 + def build(self, scan_tests, test_filter_re): 1.199 + # process the top module, which recurses to process everything it 1.200 + # reaches 1.201 + if "main" in self.target_cfg: 1.202 + top_mi = self.find_top(self.target_cfg) 1.203 + top_me = self.process_module(top_mi) 1.204 + self.top_path = top_me.get_path() 1.205 + self.datamaps[self.target_cfg.name] = DataMap(self.target_cfg) 1.206 + if scan_tests: 1.207 + mi = self._find_module_in_package("addon-sdk", "lib", "sdk/test/runner", []) 1.208 + self.process_module(mi) 1.209 + # also scan all test files in all packages that we use. By making 1.210 + # a copy of self.used_packagenames first, we refrain from 1.211 + # processing tests in packages that our own tests depend upon. If 1.212 + # we're running tests for package A, and either modules in A or 1.213 + # tests in A depend upon modules from package B, we *don't* want 1.214 + # to run tests for package B. 1.215 + test_modules = [] 1.216 + dirnames = self.target_cfg["tests"] 1.217 + if isinstance(dirnames, basestring): 1.218 + dirnames = [dirnames] 1.219 + dirnames = [os.path.join(self.target_cfg.root_dir, d) 1.220 + for d in dirnames] 1.221 + for d in dirnames: 1.222 + for filename in os.listdir(d): 1.223 + if filename.startswith("test-") and filename.endswith(".js"): 1.224 + testname = filename[:-3] # require(testname) 1.225 + if test_filter_re: 1.226 + if not re.search(test_filter_re, testname): 1.227 + continue 1.228 + tmi = ModuleInfo(self.target_cfg, "tests", testname, 1.229 + os.path.join(d, filename), None) 1.230 + # scan the test's dependencies 1.231 + tme = self.process_module(tmi) 1.232 + test_modules.append( (testname, tme) ) 1.233 + # also add it as an artificial dependency of unit-test-finder, so 1.234 + # the runtime dynamic load can work. 1.235 + test_finder = self.get_manifest_entry("addon-sdk", "lib", 1.236 + "sdk/deprecated/unit-test-finder") 1.237 + for (testname,tme) in test_modules: 1.238 + test_finder.add_requirement(testname, tme) 1.239 + # finally, tell the runtime about it, so they won't have to 1.240 + # search for all tests. self.test_modules will be passed 1.241 + # through the harness-options.json file in the 1.242 + # .allTestModules property. 1.243 + # Pass the absolute module path. 1.244 + self.test_modules.append(tme.get_path()) 1.245 + 1.246 + # include files used by the loader 1.247 + for em in self.extra_modules: 1.248 + (pkgname, section, modname, js) = em 1.249 + mi = ModuleInfo(self.pkg_cfg.packages[pkgname], section, modname, 1.250 + js, None) 1.251 + self.process_module(mi) 1.252 + 1.253 + 1.254 + def get_module_entries(self): 1.255 + return frozenset(self.manifest.values()) 1.256 + def get_data_entries(self): 1.257 + return frozenset(self.datamaps.values()) 1.258 + 1.259 + def get_used_packages(self): 1.260 + used = set() 1.261 + for index in self.manifest: 1.262 + (package, section, module) = index 1.263 + used.add(package) 1.264 + return sorted(used) 1.265 + 1.266 + def get_used_files(self, bundle_sdk_modules): 1.267 + # returns all .js files that we reference, plus data/ files. You will 1.268 + # need to add the loader, off-manifest files that it needs, and 1.269 + # generated metadata. 1.270 + for datamap in self.datamaps.values(): 1.271 + for (zipname, absname) in datamap.files_to_copy: 1.272 + yield absname 1.273 + 1.274 + for me in self.get_module_entries(): 1.275 + # Only ship SDK files if we are told to do so 1.276 + if me.packageName != "addon-sdk" or bundle_sdk_modules: 1.277 + yield me.js_filename 1.278 + 1.279 + def get_all_test_modules(self): 1.280 + return self.test_modules 1.281 + 1.282 + def get_harness_options_manifest(self, bundle_sdk_modules): 1.283 + manifest = {} 1.284 + for me in self.get_module_entries(): 1.285 + path = me.get_path() 1.286 + # Do not add manifest entries for system modules. 1.287 + # Doesn't prevent from shipping modules. 1.288 + # Shipping modules is decided in `get_used_files`. 1.289 + if me.packageName != "addon-sdk" or bundle_sdk_modules: 1.290 + manifest[path] = me.get_entry_for_manifest() 1.291 + return manifest 1.292 + 1.293 + def get_manifest_entry(self, package, section, module): 1.294 + index = (package, section, module) 1.295 + if index not in self.manifest: 1.296 + m = self.manifest[index] = ManifestEntry() 1.297 + m.packageName = package 1.298 + m.sectionName = section 1.299 + m.moduleName = module 1.300 + self.used_packagenames.add(package) 1.301 + return self.manifest[index] 1.302 + 1.303 + def uri_name_from_path(self, pkg, fn): 1.304 + # given a filename like .../pkg1/lib/bar/foo.js, and a package 1.305 + # specification (with a .root_dir like ".../pkg1" and a .lib list of 1.306 + # paths where .lib[0] is like "lib"), return the appropriate NAME 1.307 + # that can be put into a URI like resource://JID-pkg1-lib/NAME . This 1.308 + # will throw an exception if the file is outside of the lib/ 1.309 + # directory, since that means we can't construct a URI that points to 1.310 + # it. 1.311 + # 1.312 + # This should be a lot easier, and shouldn't fail when the file is in 1.313 + # the root of the package. Both should become possible when the XPI 1.314 + # is rearranged and our URI scheme is simplified. 1.315 + fn = os.path.abspath(fn) 1.316 + pkglib = pkg.lib[0] 1.317 + libdir = os.path.abspath(os.path.join(pkg.root_dir, pkglib)) 1.318 + # AARGH, section and name! we need to reverse-engineer a 1.319 + # ModuleInfo instance that will produce a URI (in the form 1.320 + # PREFIX/PKGNAME-SECTION/JS) that will map to the existing file. 1.321 + # Until we fix URI generation to get rid of "sections", this is 1.322 + # limited to files in the same .directories.lib as the rest of 1.323 + # the package uses. So if the package's main files are in lib/, 1.324 + # but the main.js is in the package root, there is no URI we can 1.325 + # construct that will point to it, and we must fail. 1.326 + # 1.327 + # This will become much easier (and the failure case removed) 1.328 + # when we get rid of sections and change the URIs to look like 1.329 + # (PREFIX/PKGNAME/PATH-TO-JS). 1.330 + 1.331 + # AARGH 2, allowing .lib to be a list is really getting in the 1.332 + # way. That needs to go away eventually too. 1.333 + if not fn.startswith(libdir): 1.334 + 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." 1.335 + % (fn, pkg.name, pkglib)) 1.336 + name = fn[len(libdir):].lstrip(SEP)[:-len(".js")] 1.337 + return name 1.338 + 1.339 + 1.340 + def parse_main(self, root_dir, main, check_lib_dir=None): 1.341 + # 'main' can be like one of the following: 1.342 + # a: ./lib/main.js b: ./lib/main c: lib/main 1.343 + # we require it to be a path to the file, though, and ignore the 1.344 + # .directories stuff. So just "main" is insufficient if you really 1.345 + # want something in a "lib/" subdirectory. 1.346 + if main.endswith(".js"): 1.347 + main = main[:-len(".js")] 1.348 + if main.startswith("./"): 1.349 + main = main[len("./"):] 1.350 + # package.json must always use "/", but on windows we'll replace that 1.351 + # with "\" before using it as an actual filename 1.352 + main = os.sep.join(main.split("/")) 1.353 + paths = [os.path.join(root_dir, main+".js")] 1.354 + if check_lib_dir is not None: 1.355 + paths.append(os.path.join(root_dir, check_lib_dir, main+".js")) 1.356 + return paths 1.357 + 1.358 + def find_top_js(self, target_cfg): 1.359 + for libdir in target_cfg.lib: 1.360 + for n in self.parse_main(target_cfg.root_dir, target_cfg.main, 1.361 + libdir): 1.362 + if os.path.exists(n): 1.363 + return n 1.364 + raise KeyError("unable to find main module '%s.js' in top-level package" % target_cfg.main) 1.365 + 1.366 + def find_top(self, target_cfg): 1.367 + top_js = self.find_top_js(target_cfg) 1.368 + n = os.path.join(target_cfg.root_dir, "README.md") 1.369 + if os.path.exists(n): 1.370 + top_docs = n 1.371 + else: 1.372 + top_docs = None 1.373 + name = self.uri_name_from_path(target_cfg, top_js) 1.374 + return ModuleInfo(target_cfg, "lib", name, top_js, top_docs) 1.375 + 1.376 + def process_module(self, mi): 1.377 + pkg = mi.package 1.378 + #print "ENTERING", pkg.name, mi.name 1.379 + # mi.name must be fully-qualified 1.380 + assert (not mi.name.startswith("./") and 1.381 + not mi.name.startswith("../")) 1.382 + # create and claim the manifest row first 1.383 + me = self.get_manifest_entry(pkg.name, mi.section, mi.name) 1.384 + 1.385 + me.add_js(mi.js) 1.386 + if mi.docs: 1.387 + me.add_docs(mi.docs) 1.388 + 1.389 + js_lines = open(mi.js,"r").readlines() 1.390 + requires, problems, locations = scan_module(mi.js,js_lines,self.stderr) 1.391 + if problems: 1.392 + # the relevant instructions have already been written to stderr 1.393 + raise BadChromeMarkerError() 1.394 + 1.395 + # We update our requirements on the way out of the depth-first 1.396 + # traversal of the module graph 1.397 + 1.398 + for reqname in sorted(requires.keys()): 1.399 + # If requirement is chrome or a pseudo-module (starts with @) make 1.400 + # path a requirement name. 1.401 + if reqname == "chrome" or reqname.startswith("@"): 1.402 + me.add_requirement(reqname, reqname) 1.403 + else: 1.404 + # when two modules require() the same name, do they get a 1.405 + # shared instance? This is a deep question. For now say yes. 1.406 + 1.407 + # find_req_for() returns an entry to put in our 1.408 + # 'requirements' dict, and will recursively process 1.409 + # everything transitively required from here. It will also 1.410 + # populate the self.modules[] cache. Note that we must 1.411 + # tolerate cycles in the reference graph. 1.412 + looked_in = [] # populated by subroutines 1.413 + them_me = self.find_req_for(mi, reqname, looked_in, locations) 1.414 + if them_me is None: 1.415 + if mi.section == "tests": 1.416 + # tolerate missing modules in tests, because 1.417 + # test-securable-module.js, and the modules/red.js 1.418 + # that it imports, both do that intentionally 1.419 + continue 1.420 + lineno = locations.get(reqname) # None means define() 1.421 + if lineno is None: 1.422 + reqtype = "define" 1.423 + else: 1.424 + reqtype = "require" 1.425 + err = ModuleNotFoundError(reqtype, reqname, 1.426 + mi.js, lineno, looked_in) 1.427 + raise err 1.428 + else: 1.429 + me.add_requirement(reqname, them_me) 1.430 + 1.431 + return me 1.432 + #print "LEAVING", pkg.name, mi.name 1.433 + 1.434 + def find_req_for(self, from_module, reqname, looked_in, locations): 1.435 + # handle a single require(reqname) statement from from_module . 1.436 + # Return a uri that exists in self.manifest 1.437 + # Populate looked_in with places we looked. 1.438 + def BAD(msg): 1.439 + return BadModuleIdentifier(msg + " in require(%s) from %s" % 1.440 + (reqname, from_module)) 1.441 + 1.442 + if not reqname: 1.443 + raise BAD("no actual modulename") 1.444 + 1.445 + # Allow things in tests/*.js to require both test code and real code. 1.446 + # But things in lib/*.js can only require real code. 1.447 + if from_module.section == "tests": 1.448 + lookfor_sections = ["tests", "lib"] 1.449 + elif from_module.section == "lib": 1.450 + lookfor_sections = ["lib"] 1.451 + else: 1.452 + raise BadSection(from_module.section) 1.453 + modulename = from_module.name 1.454 + 1.455 + #print " %s require(%s))" % (from_module, reqname) 1.456 + 1.457 + if reqname.startswith("./") or reqname.startswith("../"): 1.458 + # 1: they want something relative to themselves, always from 1.459 + # their own package 1.460 + them = modulename.split("/")[:-1] 1.461 + bits = reqname.split("/") 1.462 + while bits[0] in (".", ".."): 1.463 + if not bits: 1.464 + raise BAD("no actual modulename") 1.465 + if bits[0] == "..": 1.466 + if not them: 1.467 + raise BAD("too many ..") 1.468 + them.pop() 1.469 + bits.pop(0) 1.470 + bits = them+bits 1.471 + lookfor_pkg = from_module.package.name 1.472 + lookfor_mod = "/".join(bits) 1.473 + return self._get_module_from_package(lookfor_pkg, 1.474 + lookfor_sections, lookfor_mod, 1.475 + looked_in) 1.476 + 1.477 + # non-relative import. Might be a short name (requiring a search 1.478 + # through "library" packages), or a fully-qualified one. 1.479 + 1.480 + if "/" in reqname: 1.481 + # 2: PKG/MOD: find PKG, look inside for MOD 1.482 + bits = reqname.split("/") 1.483 + lookfor_pkg = bits[0] 1.484 + lookfor_mod = "/".join(bits[1:]) 1.485 + mi = self._get_module_from_package(lookfor_pkg, 1.486 + lookfor_sections, lookfor_mod, 1.487 + looked_in) 1.488 + if mi: # caution, 0==None 1.489 + return mi 1.490 + else: 1.491 + # 3: try finding PKG, if found, use its main.js entry point 1.492 + lookfor_pkg = reqname 1.493 + mi = self._get_entrypoint_from_package(lookfor_pkg, looked_in) 1.494 + if mi: 1.495 + return mi 1.496 + 1.497 + # 4: search packages for MOD or MODPARENT/MODCHILD. We always search 1.498 + # their own package first, then the list of packages defined by their 1.499 + # .dependencies list 1.500 + from_pkg = from_module.package.name 1.501 + mi = self._search_packages_for_module(from_pkg, 1.502 + lookfor_sections, reqname, 1.503 + looked_in) 1.504 + if mi: 1.505 + return mi 1.506 + 1.507 + # Only after we look for module in the addon itself, search for a module 1.508 + # in new layout. 1.509 + # First normalize require argument in order to easily find a mapping 1.510 + normalized = reqname 1.511 + if normalized.endswith(".js"): 1.512 + normalized = normalized[:-len(".js")] 1.513 + if normalized.startswith("addon-kit/"): 1.514 + normalized = normalized[len("addon-kit/"):] 1.515 + if normalized.startswith("api-utils/"): 1.516 + normalized = normalized[len("api-utils/"):] 1.517 + if normalized in NEW_LAYOUT_MAPPING: 1.518 + # get the new absolute path for this module 1.519 + original_reqname = reqname 1.520 + reqname = NEW_LAYOUT_MAPPING[normalized] 1.521 + from_pkg = from_module.package.name 1.522 + 1.523 + # If the addon didn't explicitely told us to ignore deprecated 1.524 + # require path, warn the developer: 1.525 + # (target_cfg is the package.json file) 1.526 + if not "ignore-deprecated-path" in self.target_cfg: 1.527 + lineno = locations.get(original_reqname) 1.528 + print >>self.stderr, "Warning: Use of deprecated require path:" 1.529 + print >>self.stderr, " In %s:%d:" % (from_module.js, lineno) 1.530 + print >>self.stderr, " require('%s')." % original_reqname 1.531 + print >>self.stderr, " New path should be:" 1.532 + print >>self.stderr, " require('%s')" % reqname 1.533 + 1.534 + return self._search_packages_for_module(from_pkg, 1.535 + lookfor_sections, reqname, 1.536 + looked_in) 1.537 + else: 1.538 + # We weren't able to find this module, really. 1.539 + return None 1.540 + 1.541 + def _handle_module(self, mi): 1.542 + if not mi: 1.543 + return None 1.544 + 1.545 + # we tolerate cycles in the reference graph, which means we need to 1.546 + # populate the self.modules cache before recursing into 1.547 + # process_module() . We must also check the cache first, so recursion 1.548 + # can terminate. 1.549 + if mi in self.modules: 1.550 + return self.modules[mi] 1.551 + 1.552 + # this creates the entry 1.553 + new_entry = self.get_manifest_entry(mi.package.name, mi.section, mi.name) 1.554 + # and populates the cache 1.555 + self.modules[mi] = new_entry 1.556 + self.process_module(mi) 1.557 + return new_entry 1.558 + 1.559 + def _get_module_from_package(self, pkgname, sections, modname, looked_in): 1.560 + if pkgname not in self.pkg_cfg.packages: 1.561 + return None 1.562 + mi = self._find_module_in_package(pkgname, sections, modname, 1.563 + looked_in) 1.564 + return self._handle_module(mi) 1.565 + 1.566 + def _get_entrypoint_from_package(self, pkgname, looked_in): 1.567 + if pkgname not in self.pkg_cfg.packages: 1.568 + return None 1.569 + pkg = self.pkg_cfg.packages[pkgname] 1.570 + main = pkg.get("main", None) 1.571 + if not main: 1.572 + return None 1.573 + for js in self.parse_main(pkg.root_dir, main): 1.574 + looked_in.append(js) 1.575 + if os.path.exists(js): 1.576 + section = "lib" 1.577 + name = self.uri_name_from_path(pkg, js) 1.578 + docs = None 1.579 + mi = ModuleInfo(pkg, section, name, js, docs) 1.580 + return self._handle_module(mi) 1.581 + return None 1.582 + 1.583 + def _search_packages_for_module(self, from_pkg, sections, reqname, 1.584 + looked_in): 1.585 + searchpath = [] # list of package names 1.586 + searchpath.append(from_pkg) # search self first 1.587 + us = self.pkg_cfg.packages[from_pkg] 1.588 + if 'dependencies' in us: 1.589 + # only look in dependencies 1.590 + searchpath.extend(us['dependencies']) 1.591 + else: 1.592 + # they didn't declare any dependencies (or they declared an empty 1.593 + # list, but we'll treat that as not declaring one, because it's 1.594 + # easier), so look in all deps, sorted alphabetically, so 1.595 + # addon-kit comes first. Note that self.deps includes all 1.596 + # packages found by traversing the ".dependencies" lists in each 1.597 + # package.json, starting from the main addon package, plus 1.598 + # everything added by --extra-packages 1.599 + searchpath.extend(sorted(self.deps)) 1.600 + for pkgname in searchpath: 1.601 + mi = self._find_module_in_package(pkgname, sections, reqname, 1.602 + looked_in) 1.603 + if mi: 1.604 + return self._handle_module(mi) 1.605 + return None 1.606 + 1.607 + def _find_module_in_package(self, pkgname, sections, name, looked_in): 1.608 + # require("a/b/c") should look at ...\a\b\c.js on windows 1.609 + filename = os.sep.join(name.split("/")) 1.610 + # normalize filename, make sure that we do not add .js if it already has 1.611 + # it. 1.612 + if not filename.endswith(".js") and not filename.endswith(".json"): 1.613 + filename += ".js" 1.614 + 1.615 + if filename.endswith(".js"): 1.616 + basename = filename[:-3] 1.617 + if filename.endswith(".json"): 1.618 + basename = filename[:-5] 1.619 + 1.620 + pkg = self.pkg_cfg.packages[pkgname] 1.621 + if isinstance(sections, basestring): 1.622 + sections = [sections] 1.623 + for section in sections: 1.624 + for sdir in pkg.get(section, []): 1.625 + js = os.path.join(pkg.root_dir, sdir, filename) 1.626 + looked_in.append(js) 1.627 + if os.path.exists(js): 1.628 + docs = None 1.629 + maybe_docs = os.path.join(pkg.root_dir, "docs", 1.630 + basename+".md") 1.631 + if section == "lib" and os.path.exists(maybe_docs): 1.632 + docs = maybe_docs 1.633 + return ModuleInfo(pkg, section, name, js, docs) 1.634 + return None 1.635 + 1.636 +def build_manifest(target_cfg, pkg_cfg, deps, scan_tests, 1.637 + test_filter_re=None, extra_modules=[]): 1.638 + """ 1.639 + Perform recursive dependency analysis starting from entry_point, 1.640 + building up a manifest of modules that need to be included in the XPI. 1.641 + Each entry will map require() names to the URL of the module that will 1.642 + be used to satisfy that dependency. The manifest will be used by the 1.643 + runtime's require() code. 1.644 + 1.645 + This returns a ManifestBuilder object, with two public methods. The 1.646 + first, get_module_entries(), returns a set of ManifestEntry objects, each 1.647 + of which can be asked for the following: 1.648 + 1.649 + * its contribution to the harness-options.json '.manifest' 1.650 + * the local disk name 1.651 + * the name in the XPI at which it should be placed 1.652 + 1.653 + The second is get_data_entries(), which returns a set of DataEntry 1.654 + objects, each of which has: 1.655 + 1.656 + * local disk name 1.657 + * name in the XPI 1.658 + 1.659 + note: we don't build the XPI here, but our manifest is passed to the 1.660 + code which does, so it knows what to copy into the XPI. 1.661 + """ 1.662 + 1.663 + mxt = ManifestBuilder(target_cfg, pkg_cfg, deps, extra_modules) 1.664 + mxt.build(scan_tests, test_filter_re) 1.665 + return mxt 1.666 + 1.667 + 1.668 + 1.669 +COMMENT_PREFIXES = ["//", "/*", "*", "dump("] 1.670 + 1.671 +REQUIRE_RE = r"(?<![\'\"])require\s*\(\s*[\'\"]([^\'\"]+?)[\'\"]\s*\)" 1.672 + 1.673 +# detect the define idiom of the form: 1.674 +# define("module name", ["dep1", "dep2", "dep3"], function() {}) 1.675 +# by capturing the contents of the list in a group. 1.676 +DEF_RE = re.compile(r"(require|define)\s*\(\s*([\'\"][^\'\"]+[\'\"]\s*,)?\s*\[([^\]]+)\]") 1.677 + 1.678 +# Out of the async dependencies, do not allow quotes in them. 1.679 +DEF_RE_ALLOWED = re.compile(r"^[\'\"][^\'\"]+[\'\"]$") 1.680 + 1.681 +def scan_requirements_with_grep(fn, lines): 1.682 + requires = {} 1.683 + first_location = {} 1.684 + for (lineno0, line) in enumerate(lines): 1.685 + for clause in line.split(";"): 1.686 + clause = clause.strip() 1.687 + iscomment = False 1.688 + for commentprefix in COMMENT_PREFIXES: 1.689 + if clause.startswith(commentprefix): 1.690 + iscomment = True 1.691 + if iscomment: 1.692 + continue 1.693 + mo = re.finditer(REQUIRE_RE, clause) 1.694 + if mo: 1.695 + for mod in mo: 1.696 + modname = mod.group(1) 1.697 + requires[modname] = {} 1.698 + if modname not in first_location: 1.699 + first_location[modname] = lineno0 + 1 1.700 + 1.701 + # define() can happen across multiple lines, so join everyone up. 1.702 + wholeshebang = "\n".join(lines) 1.703 + for match in DEF_RE.finditer(wholeshebang): 1.704 + # this should net us a list of string literals separated by commas 1.705 + for strbit in match.group(3).split(","): 1.706 + strbit = strbit.strip() 1.707 + # There could be a trailing comma netting us just whitespace, so 1.708 + # filter that out. Make sure that only string values with 1.709 + # quotes around them are allowed, and no quotes are inside 1.710 + # the quoted value. 1.711 + if strbit and DEF_RE_ALLOWED.match(strbit): 1.712 + modname = strbit[1:-1] 1.713 + if modname not in ["exports"]: 1.714 + requires[modname] = {} 1.715 + # joining all the lines means we lose line numbers, so we 1.716 + # can't fill first_location[] 1.717 + 1.718 + return requires, first_location 1.719 + 1.720 +CHROME_ALIASES = [ 1.721 + (re.compile(r"Components\.classes"), "Cc"), 1.722 + (re.compile(r"Components\.interfaces"), "Ci"), 1.723 + (re.compile(r"Components\.utils"), "Cu"), 1.724 + (re.compile(r"Components\.results"), "Cr"), 1.725 + (re.compile(r"Components\.manager"), "Cm"), 1.726 + ] 1.727 +OTHER_CHROME = re.compile(r"Components\.[a-zA-Z]") 1.728 + 1.729 +def scan_for_bad_chrome(fn, lines, stderr): 1.730 + problems = False 1.731 + old_chrome = set() # i.e. "Cc" when we see "Components.classes" 1.732 + old_chrome_lines = [] # list of (lineno, line.strip()) tuples 1.733 + for lineno,line in enumerate(lines): 1.734 + # note: this scanner is not obligated to spot all possible forms of 1.735 + # chrome access. The scanner is detecting voluntary requests for 1.736 + # chrome. Runtime tools will enforce allowance or denial of access. 1.737 + line = line.strip() 1.738 + iscomment = False 1.739 + for commentprefix in COMMENT_PREFIXES: 1.740 + if line.startswith(commentprefix): 1.741 + iscomment = True 1.742 + break 1.743 + if iscomment: 1.744 + continue 1.745 + old_chrome_in_this_line = set() 1.746 + for (regexp,alias) in CHROME_ALIASES: 1.747 + if regexp.search(line): 1.748 + old_chrome_in_this_line.add(alias) 1.749 + if not old_chrome_in_this_line: 1.750 + if OTHER_CHROME.search(line): 1.751 + old_chrome_in_this_line.add("components") 1.752 + old_chrome.update(old_chrome_in_this_line) 1.753 + if old_chrome_in_this_line: 1.754 + old_chrome_lines.append( (lineno+1, line) ) 1.755 + 1.756 + if old_chrome: 1.757 + print >>stderr, """ 1.758 +The following lines from file %(fn)s: 1.759 +%(lines)s 1.760 +use 'Components' to access chrome authority. To do so, you need to add a 1.761 +line somewhat like the following: 1.762 + 1.763 + const {%(needs)s} = require("chrome"); 1.764 + 1.765 +Then you can use any shortcuts to its properties that you import from the 1.766 +'chrome' module ('Cc', 'Ci', 'Cm', 'Cr', and 'Cu' for the 'classes', 1.767 +'interfaces', 'manager', 'results', and 'utils' properties, respectively. And 1.768 +`components` for `Components` object itself). 1.769 +""" % { "fn": fn, "needs": ",".join(sorted(old_chrome)), 1.770 + "lines": "\n".join([" %3d: %s" % (lineno,line) 1.771 + for (lineno, line) in old_chrome_lines]), 1.772 + } 1.773 + problems = True 1.774 + return problems 1.775 + 1.776 +def scan_module(fn, lines, stderr=sys.stderr): 1.777 + filename = os.path.basename(fn) 1.778 + requires, locations = scan_requirements_with_grep(fn, lines) 1.779 + if filename == "cuddlefish.js": 1.780 + # this is the loader: don't scan for chrome 1.781 + problems = False 1.782 + else: 1.783 + problems = scan_for_bad_chrome(fn, lines, stderr) 1.784 + return requires, problems, locations 1.785 + 1.786 + 1.787 + 1.788 +if __name__ == '__main__': 1.789 + for fn in sys.argv[1:]: 1.790 + requires, problems, locations = scan_module(fn, open(fn).readlines()) 1.791 + print 1.792 + print "---", fn 1.793 + if problems: 1.794 + print "PROBLEMS" 1.795 + sys.exit(1) 1.796 + print "requires: %s" % (",".join(sorted(requires.keys()))) 1.797 + print "locations: %s" % locations 1.798 +