Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | |
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 |