addon-sdk/source/python-lib/cuddlefish/packaging.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 import os
michael@0 6 import sys
michael@0 7 import re
michael@0 8 import copy
michael@0 9
michael@0 10 import simplejson as json
michael@0 11 from cuddlefish.bunch import Bunch
michael@0 12
michael@0 13 MANIFEST_NAME = 'package.json'
michael@0 14 DEFAULT_LOADER = 'addon-sdk'
michael@0 15
michael@0 16 # Is different from root_dir when running tests
michael@0 17 env_root = os.environ.get('CUDDLEFISH_ROOT')
michael@0 18
michael@0 19 DEFAULT_PROGRAM_MODULE = 'main'
michael@0 20
michael@0 21 DEFAULT_ICON = 'icon.png'
michael@0 22 DEFAULT_ICON64 = 'icon64.png'
michael@0 23
michael@0 24 METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version',
michael@0 25 'translators', 'contributors', 'license', 'homepage', 'icon',
michael@0 26 'icon64', 'main', 'directories', 'permissions']
michael@0 27
michael@0 28 RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$')
michael@0 29
michael@0 30 class Error(Exception):
michael@0 31 pass
michael@0 32
michael@0 33 class MalformedPackageError(Error):
michael@0 34 pass
michael@0 35
michael@0 36 class MalformedJsonFileError(Error):
michael@0 37 pass
michael@0 38
michael@0 39 class DuplicatePackageError(Error):
michael@0 40 pass
michael@0 41
michael@0 42 class PackageNotFoundError(Error):
michael@0 43 def __init__(self, missing_package, reason):
michael@0 44 self.missing_package = missing_package
michael@0 45 self.reason = reason
michael@0 46 def __str__(self):
michael@0 47 return "%s (%s)" % (self.missing_package, self.reason)
michael@0 48
michael@0 49 class BadChromeMarkerError(Error):
michael@0 50 pass
michael@0 51
michael@0 52 def validate_resource_hostname(name):
michael@0 53 """
michael@0 54 Validates the given hostname for a resource: URI.
michael@0 55
michael@0 56 For more information, see:
michael@0 57
michael@0 58 https://bugzilla.mozilla.org/show_bug.cgi?id=566812#c13
michael@0 59
michael@0 60 Examples:
michael@0 61
michael@0 62 >>> validate_resource_hostname('blarg')
michael@0 63
michael@0 64 >>> validate_resource_hostname('bl arg')
michael@0 65 Traceback (most recent call last):
michael@0 66 ...
michael@0 67 ValueError: Error: the name of your package contains an invalid character.
michael@0 68 Package names can contain only lower-case letters, numbers, underscores, and dashes.
michael@0 69 Current package name: bl arg
michael@0 70
michael@0 71 >>> validate_resource_hostname('BLARG')
michael@0 72 Traceback (most recent call last):
michael@0 73 ...
michael@0 74 ValueError: Error: the name of your package contains upper-case letters.
michael@0 75 Package names can contain only lower-case letters, numbers, underscores, and dashes.
michael@0 76 Current package name: BLARG
michael@0 77
michael@0 78 >>> validate_resource_hostname('foo@bar')
michael@0 79 Traceback (most recent call last):
michael@0 80 ...
michael@0 81 ValueError: Error: the name of your package contains an invalid character.
michael@0 82 Package names can contain only lower-case letters, numbers, underscores, and dashes.
michael@0 83 Current package name: foo@bar
michael@0 84 """
michael@0 85
michael@0 86 # See https://bugzilla.mozilla.org/show_bug.cgi?id=568131 for details.
michael@0 87 if not name.islower():
michael@0 88 raise ValueError("""Error: the name of your package contains upper-case letters.
michael@0 89 Package names can contain only lower-case letters, numbers, underscores, and dashes.
michael@0 90 Current package name: %s""" % name)
michael@0 91
michael@0 92 if not RESOURCE_HOSTNAME_RE.match(name):
michael@0 93 raise ValueError("""Error: the name of your package contains an invalid character.
michael@0 94 Package names can contain only lower-case letters, numbers, underscores, and dashes.
michael@0 95 Current package name: %s""" % name)
michael@0 96
michael@0 97 def find_packages_with_module(pkg_cfg, name):
michael@0 98 # TODO: Make this support more than just top-level modules.
michael@0 99 filename = "%s.js" % name
michael@0 100 packages = []
michael@0 101 for cfg in pkg_cfg.packages.itervalues():
michael@0 102 if 'lib' in cfg:
michael@0 103 matches = [dirname for dirname in resolve_dirs(cfg, cfg.lib)
michael@0 104 if os.path.exists(os.path.join(dirname, filename))]
michael@0 105 if matches:
michael@0 106 packages.append(cfg.name)
michael@0 107 return packages
michael@0 108
michael@0 109 def resolve_dirs(pkg_cfg, dirnames):
michael@0 110 for dirname in dirnames:
michael@0 111 yield resolve_dir(pkg_cfg, dirname)
michael@0 112
michael@0 113 def resolve_dir(pkg_cfg, dirname):
michael@0 114 return os.path.join(pkg_cfg.root_dir, dirname)
michael@0 115
michael@0 116 def validate_permissions(perms):
michael@0 117 if (perms.get('cross-domain-content') and
michael@0 118 not isinstance(perms.get('cross-domain-content'), list)):
michael@0 119 raise ValueError("Error: `cross-domain-content` permissions in \
michael@0 120 package.json file must be an array of strings:\n %s" % perms)
michael@0 121
michael@0 122 def get_metadata(pkg_cfg, deps):
michael@0 123 metadata = Bunch()
michael@0 124 for pkg_name in deps:
michael@0 125 cfg = pkg_cfg.packages[pkg_name]
michael@0 126 metadata[pkg_name] = Bunch()
michael@0 127 for prop in METADATA_PROPS:
michael@0 128 if cfg.get(prop):
michael@0 129 if prop == 'permissions':
michael@0 130 validate_permissions(cfg[prop])
michael@0 131 metadata[pkg_name][prop] = cfg[prop]
michael@0 132 return metadata
michael@0 133
michael@0 134 def set_section_dir(base_json, name, base_path, dirnames, allow_root=False):
michael@0 135 resolved = compute_section_dir(base_json, base_path, dirnames, allow_root)
michael@0 136 if resolved:
michael@0 137 base_json[name] = os.path.abspath(resolved)
michael@0 138
michael@0 139 def compute_section_dir(base_json, base_path, dirnames, allow_root):
michael@0 140 # PACKAGE_JSON.lib is highest priority
michael@0 141 # then PACKAGE_JSON.directories.lib
michael@0 142 # then lib/ (if it exists)
michael@0 143 # then . (but only if allow_root=True)
michael@0 144 for dirname in dirnames:
michael@0 145 if base_json.get(dirname):
michael@0 146 return os.path.join(base_path, base_json[dirname])
michael@0 147 if "directories" in base_json:
michael@0 148 for dirname in dirnames:
michael@0 149 if dirname in base_json.directories:
michael@0 150 return os.path.join(base_path, base_json.directories[dirname])
michael@0 151 for dirname in dirnames:
michael@0 152 if os.path.isdir(os.path.join(base_path, dirname)):
michael@0 153 return os.path.join(base_path, dirname)
michael@0 154 if allow_root:
michael@0 155 return os.path.abspath(base_path)
michael@0 156 return None
michael@0 157
michael@0 158 def normalize_string_or_array(base_json, key):
michael@0 159 if base_json.get(key):
michael@0 160 if isinstance(base_json[key], basestring):
michael@0 161 base_json[key] = [base_json[key]]
michael@0 162
michael@0 163 def load_json_file(path):
michael@0 164 data = open(path, 'r').read()
michael@0 165 try:
michael@0 166 return Bunch(json.loads(data))
michael@0 167 except ValueError, e:
michael@0 168 raise MalformedJsonFileError('%s when reading "%s"' % (str(e),
michael@0 169 path))
michael@0 170
michael@0 171 def get_config_in_dir(path):
michael@0 172 package_json = os.path.join(path, MANIFEST_NAME)
michael@0 173 if not (os.path.exists(package_json) and
michael@0 174 os.path.isfile(package_json)):
michael@0 175 raise MalformedPackageError('%s not found in "%s"' % (MANIFEST_NAME,
michael@0 176 path))
michael@0 177 base_json = load_json_file(package_json)
michael@0 178
michael@0 179 if 'name' not in base_json:
michael@0 180 base_json.name = os.path.basename(path)
michael@0 181
michael@0 182 # later processing steps will expect to see the following keys in the
michael@0 183 # base_json that we return:
michael@0 184 #
michael@0 185 # name: name of the package
michael@0 186 # lib: list of directories with .js files
michael@0 187 # test: list of directories with test-*.js files
michael@0 188 # doc: list of directories with documentation .md files
michael@0 189 # data: list of directories with bundled arbitrary data files
michael@0 190 # packages: ?
michael@0 191
michael@0 192 if (not base_json.get('tests') and
michael@0 193 os.path.isdir(os.path.join(path, 'test'))):
michael@0 194 base_json['tests'] = 'test'
michael@0 195
michael@0 196 set_section_dir(base_json, 'lib', path, ['lib'], True)
michael@0 197 set_section_dir(base_json, 'tests', path, ['test', 'tests'], False)
michael@0 198 set_section_dir(base_json, 'doc', path, ['doc', 'docs'])
michael@0 199 set_section_dir(base_json, 'data', path, ['data'])
michael@0 200 set_section_dir(base_json, 'packages', path, ['packages'])
michael@0 201 set_section_dir(base_json, 'locale', path, ['locale'])
michael@0 202
michael@0 203 if (not base_json.get('icon') and
michael@0 204 os.path.isfile(os.path.join(path, DEFAULT_ICON))):
michael@0 205 base_json['icon'] = DEFAULT_ICON
michael@0 206
michael@0 207 if (not base_json.get('icon64') and
michael@0 208 os.path.isfile(os.path.join(path, DEFAULT_ICON64))):
michael@0 209 base_json['icon64'] = DEFAULT_ICON64
michael@0 210
michael@0 211 for key in ['lib', 'tests', 'dependencies', 'packages']:
michael@0 212 # TODO: lib/tests can be an array?? consider interaction with
michael@0 213 # compute_section_dir above
michael@0 214 normalize_string_or_array(base_json, key)
michael@0 215
michael@0 216 if 'main' not in base_json and 'lib' in base_json:
michael@0 217 for dirname in base_json['lib']:
michael@0 218 program = os.path.join(path, dirname,
michael@0 219 '%s.js' % DEFAULT_PROGRAM_MODULE)
michael@0 220 if os.path.exists(program):
michael@0 221 base_json['main'] = DEFAULT_PROGRAM_MODULE
michael@0 222 break
michael@0 223
michael@0 224 base_json.root_dir = path
michael@0 225
michael@0 226 if "dependencies" in base_json:
michael@0 227 deps = base_json["dependencies"]
michael@0 228 deps = [x for x in deps if x not in ["addon-kit", "api-utils"]]
michael@0 229 deps.append("addon-sdk")
michael@0 230 base_json["dependencies"] = deps
michael@0 231
michael@0 232 return base_json
michael@0 233
michael@0 234 def _is_same_file(a, b):
michael@0 235 if hasattr(os.path, 'samefile'):
michael@0 236 return os.path.samefile(a, b)
michael@0 237 return a == b
michael@0 238
michael@0 239 def build_config(root_dir, target_cfg, packagepath=[]):
michael@0 240 dirs_to_scan = [env_root] # root is addon-sdk dir, diff from root_dir in tests
michael@0 241
michael@0 242 def add_packages_from_config(pkgconfig):
michael@0 243 if 'packages' in pkgconfig:
michael@0 244 for package_dir in resolve_dirs(pkgconfig, pkgconfig.packages):
michael@0 245 dirs_to_scan.append(package_dir)
michael@0 246
michael@0 247 add_packages_from_config(target_cfg)
michael@0 248
michael@0 249 packages_dir = os.path.join(root_dir, 'packages')
michael@0 250 if os.path.exists(packages_dir) and os.path.isdir(packages_dir):
michael@0 251 dirs_to_scan.append(packages_dir)
michael@0 252 dirs_to_scan.extend(packagepath)
michael@0 253
michael@0 254 packages = Bunch({target_cfg.name: target_cfg})
michael@0 255
michael@0 256 while dirs_to_scan:
michael@0 257 packages_dir = dirs_to_scan.pop()
michael@0 258 if os.path.exists(os.path.join(packages_dir, "package.json")):
michael@0 259 package_paths = [packages_dir]
michael@0 260 else:
michael@0 261 package_paths = [os.path.join(packages_dir, dirname)
michael@0 262 for dirname in os.listdir(packages_dir)
michael@0 263 if not dirname.startswith('.')]
michael@0 264 package_paths = [dirname for dirname in package_paths
michael@0 265 if os.path.isdir(dirname)]
michael@0 266
michael@0 267 for path in package_paths:
michael@0 268 pkgconfig = get_config_in_dir(path)
michael@0 269 if pkgconfig.name in packages:
michael@0 270 otherpkg = packages[pkgconfig.name]
michael@0 271 if not _is_same_file(otherpkg.root_dir, path):
michael@0 272 raise DuplicatePackageError(path, otherpkg.root_dir)
michael@0 273 else:
michael@0 274 packages[pkgconfig.name] = pkgconfig
michael@0 275 add_packages_from_config(pkgconfig)
michael@0 276
michael@0 277 return Bunch(packages=packages)
michael@0 278
michael@0 279 def get_deps_for_targets(pkg_cfg, targets):
michael@0 280 visited = []
michael@0 281 deps_left = [[dep, None] for dep in list(targets)]
michael@0 282
michael@0 283 while deps_left:
michael@0 284 [dep, required_by] = deps_left.pop()
michael@0 285 if dep not in visited:
michael@0 286 visited.append(dep)
michael@0 287 if dep not in pkg_cfg.packages:
michael@0 288 required_reason = ("required by '%s'" % (required_by)) \
michael@0 289 if required_by is not None \
michael@0 290 else "specified as target"
michael@0 291 raise PackageNotFoundError(dep, required_reason)
michael@0 292 dep_cfg = pkg_cfg.packages[dep]
michael@0 293 deps_left.extend([[i, dep] for i in dep_cfg.get('dependencies', [])])
michael@0 294 deps_left.extend([[i, dep] for i in dep_cfg.get('extra_dependencies', [])])
michael@0 295
michael@0 296 return visited
michael@0 297
michael@0 298 def generate_build_for_target(pkg_cfg, target, deps,
michael@0 299 include_tests=True,
michael@0 300 include_dep_tests=False,
michael@0 301 is_running_tests=False,
michael@0 302 default_loader=DEFAULT_LOADER):
michael@0 303
michael@0 304 build = Bunch(# Contains section directories for all packages:
michael@0 305 packages=Bunch(),
michael@0 306 locale=Bunch()
michael@0 307 )
michael@0 308
michael@0 309 def add_section_to_build(cfg, section, is_code=False,
michael@0 310 is_data=False):
michael@0 311 if section in cfg:
michael@0 312 dirnames = cfg[section]
michael@0 313 if isinstance(dirnames, basestring):
michael@0 314 # This is just for internal consistency within this
michael@0 315 # function, it has nothing to do w/ a non-canonical
michael@0 316 # configuration dict.
michael@0 317 dirnames = [dirnames]
michael@0 318 for dirname in resolve_dirs(cfg, dirnames):
michael@0 319 # ensure that package name is valid
michael@0 320 try:
michael@0 321 validate_resource_hostname(cfg.name)
michael@0 322 except ValueError, err:
michael@0 323 print err
michael@0 324 sys.exit(1)
michael@0 325 # ensure that this package has an entry
michael@0 326 if not cfg.name in build.packages:
michael@0 327 build.packages[cfg.name] = Bunch()
michael@0 328 # detect duplicated sections
michael@0 329 if section in build.packages[cfg.name]:
michael@0 330 raise KeyError("package's section already defined",
michael@0 331 cfg.name, section)
michael@0 332 # Register this section (lib, data, tests)
michael@0 333 build.packages[cfg.name][section] = dirname
michael@0 334
michael@0 335 def add_locale_to_build(cfg):
michael@0 336 # Bug 730776: Ignore locales for addon-kit, that are only for unit tests
michael@0 337 if not is_running_tests and cfg.name == "addon-sdk":
michael@0 338 return
michael@0 339
michael@0 340 path = resolve_dir(cfg, cfg['locale'])
michael@0 341 files = os.listdir(path)
michael@0 342 for filename in files:
michael@0 343 fullpath = os.path.join(path, filename)
michael@0 344 if os.path.isfile(fullpath) and filename.endswith('.properties'):
michael@0 345 language = filename[:-len('.properties')]
michael@0 346
michael@0 347 from property_parser import parse_file, MalformedLocaleFileError
michael@0 348 try:
michael@0 349 content = parse_file(fullpath)
michael@0 350 except MalformedLocaleFileError, msg:
michael@0 351 print msg[0]
michael@0 352 sys.exit(1)
michael@0 353
michael@0 354 # Merge current locales into global locale hashtable.
michael@0 355 # Locale files only contains one big JSON object
michael@0 356 # that act as an hastable of:
michael@0 357 # "keys to translate" => "translated keys"
michael@0 358 if language in build.locale:
michael@0 359 merge = (build.locale[language].items() +
michael@0 360 content.items())
michael@0 361 build.locale[language] = Bunch(merge)
michael@0 362 else:
michael@0 363 build.locale[language] = content
michael@0 364
michael@0 365 def add_dep_to_build(dep):
michael@0 366 dep_cfg = pkg_cfg.packages[dep]
michael@0 367 add_section_to_build(dep_cfg, "lib", is_code=True)
michael@0 368 add_section_to_build(dep_cfg, "data", is_data=True)
michael@0 369 if include_tests and include_dep_tests:
michael@0 370 add_section_to_build(dep_cfg, "tests", is_code=True)
michael@0 371 if 'locale' in dep_cfg:
michael@0 372 add_locale_to_build(dep_cfg)
michael@0 373 if ("loader" in dep_cfg) and ("loader" not in build):
michael@0 374 build.loader = "%s/%s" % (dep,
michael@0 375 dep_cfg.loader)
michael@0 376
michael@0 377 target_cfg = pkg_cfg.packages[target]
michael@0 378
michael@0 379 if include_tests and not include_dep_tests:
michael@0 380 add_section_to_build(target_cfg, "tests", is_code=True)
michael@0 381
michael@0 382 for dep in deps:
michael@0 383 add_dep_to_build(dep)
michael@0 384
michael@0 385 if 'loader' not in build:
michael@0 386 add_dep_to_build(DEFAULT_LOADER)
michael@0 387
michael@0 388 if 'icon' in target_cfg:
michael@0 389 build['icon'] = os.path.join(target_cfg.root_dir, target_cfg.icon)
michael@0 390 del target_cfg['icon']
michael@0 391
michael@0 392 if 'icon64' in target_cfg:
michael@0 393 build['icon64'] = os.path.join(target_cfg.root_dir, target_cfg.icon64)
michael@0 394 del target_cfg['icon64']
michael@0 395
michael@0 396 if ('preferences' in target_cfg):
michael@0 397 build['preferences'] = target_cfg.preferences
michael@0 398
michael@0 399 if 'id' in target_cfg:
michael@0 400 # NOTE: logic duplicated from buildJID()
michael@0 401 jid = target_cfg['id']
michael@0 402 if not ('@' in jid or jid.startswith('{')):
michael@0 403 jid += '@jetpack'
michael@0 404 build['preferencesBranch'] = jid
michael@0 405
michael@0 406 if 'preferences-branch' in target_cfg:
michael@0 407 # check it's a non-empty, valid branch name
michael@0 408 preferencesBranch = target_cfg['preferences-branch']
michael@0 409 if re.match('^[\w{@}-]+$', preferencesBranch):
michael@0 410 build['preferencesBranch'] = preferencesBranch
michael@0 411 elif not is_running_tests:
michael@0 412 print >>sys.stderr, "IGNORING preferences-branch (not a valid branch name)"
michael@0 413
michael@0 414 return build
michael@0 415
michael@0 416 def _get_files_in_dir(path):
michael@0 417 data = {}
michael@0 418 files = os.listdir(path)
michael@0 419 for filename in files:
michael@0 420 fullpath = os.path.join(path, filename)
michael@0 421 if os.path.isdir(fullpath):
michael@0 422 data[filename] = _get_files_in_dir(fullpath)
michael@0 423 else:
michael@0 424 try:
michael@0 425 info = os.stat(fullpath)
michael@0 426 data[filename] = ("file", dict(size=info.st_size))
michael@0 427 except OSError:
michael@0 428 pass
michael@0 429 return ("directory", data)
michael@0 430
michael@0 431 def build_pkg_index(pkg_cfg):
michael@0 432 pkg_cfg = copy.deepcopy(pkg_cfg)
michael@0 433 for pkg in pkg_cfg.packages:
michael@0 434 root_dir = pkg_cfg.packages[pkg].root_dir
michael@0 435 files = _get_files_in_dir(root_dir)
michael@0 436 pkg_cfg.packages[pkg].files = files
michael@0 437 try:
michael@0 438 readme = open(root_dir + '/README.md').read()
michael@0 439 pkg_cfg.packages[pkg].readme = readme
michael@0 440 except IOError:
michael@0 441 pass
michael@0 442 del pkg_cfg.packages[pkg].root_dir
michael@0 443 return pkg_cfg.packages
michael@0 444
michael@0 445 def build_pkg_cfg(root):
michael@0 446 pkg_cfg = build_config(root, Bunch(name='dummy'))
michael@0 447 del pkg_cfg.packages['dummy']
michael@0 448 return pkg_cfg
michael@0 449
michael@0 450 def call_plugins(pkg_cfg, deps):
michael@0 451 for dep in deps:
michael@0 452 dep_cfg = pkg_cfg.packages[dep]
michael@0 453 dirnames = dep_cfg.get('python-lib', [])
michael@0 454 for dirname in resolve_dirs(dep_cfg, dirnames):
michael@0 455 sys.path.append(dirname)
michael@0 456 module_names = dep_cfg.get('python-plugins', [])
michael@0 457 for module_name in module_names:
michael@0 458 module = __import__(module_name)
michael@0 459 module.init(root_dir=dep_cfg.root_dir)
michael@0 460
michael@0 461 def call_cmdline_tool(env_root, pkg_name):
michael@0 462 pkg_cfg = build_config(env_root, Bunch(name='dummy'))
michael@0 463 if pkg_name not in pkg_cfg.packages:
michael@0 464 print "This tool requires the '%s' package." % pkg_name
michael@0 465 sys.exit(1)
michael@0 466 cfg = pkg_cfg.packages[pkg_name]
michael@0 467 for dirname in resolve_dirs(cfg, cfg['python-lib']):
michael@0 468 sys.path.append(dirname)
michael@0 469 module_name = cfg.get('python-cmdline-tool')
michael@0 470 module = __import__(module_name)
michael@0 471 module.run()

mercurial