addon-sdk/source/python-lib/cuddlefish/manifest.py

Thu, 15 Jan 2015 15:59:08 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:59:08 +0100
branch
TOR_BUG_9701
changeset 10
ac0c01689b40
permissions
-rw-r--r--

Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

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

mercurial