| Index: packagerChrome.py | 
| =================================================================== | 
| --- a/packagerChrome.py | 
| +++ b/packagerChrome.py | 
| @@ -10,127 +10,173 @@ | 
| # Adblock Plus is distributed in the hope that it will be useful, | 
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
| # GNU General Public License for more details. | 
| # | 
| # You should have received a copy of the GNU General Public License | 
| # along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
|  | 
| -import sys, os, subprocess, re, json, codecs, struct | 
| +import sys, os, subprocess, re, json, codecs, struct, jinja2, buildtools | 
| from ConfigParser import SafeConfigParser | 
| from StringIO import StringIO | 
| from zipfile import ZipFile, ZIP_DEFLATED | 
|  | 
| defaultLocale = 'en_US' | 
|  | 
| -def getDefaultFileName(baseDir, metadata, version, ext): | 
| -  return os.path.join(baseDir, '%s-%s.%s' % (metadata.get('general', 'basename'), version, ext)) | 
| +def getDefaultFileName(baseDir, metadata, ext): | 
| +  return os.path.join(baseDir, '%s-%s.%s' % (metadata.get('general', 'basename'), metadata.get('general', 'version'), ext)) | 
|  | 
| def getMetadataPath(baseDir): | 
| return os.path.join(baseDir, 'metadata') | 
|  | 
| def getBuildNum(baseDir): | 
| try: | 
| (result, dummy) = subprocess.Popen(['hg', 'id', '-n'], stdout=subprocess.PIPE).communicate() | 
| return re.sub(r'\D', '', result) | 
| except Exception: | 
| return '0' | 
|  | 
| +def getIgnoredFiles(params): | 
| +  return ['store.description'] | 
| + | 
| def readMetadata(baseDir): | 
| metadata = SafeConfigParser() | 
| metadata.optionxform = str | 
| file = codecs.open(getMetadataPath(baseDir), 'rb', encoding='utf-8') | 
| metadata.readfp(file) | 
| file.close() | 
| return metadata | 
|  | 
| -def readVersion(baseDir): | 
| -  file = open(os.path.join(baseDir, 'manifest.json')) | 
| -  data = json.load(file) | 
| -  file.close() | 
| -  return data['version'] | 
| +def getPackageFiles(params): | 
| +  baseDir = params['baseDir'] | 
| +  for file in ('_locales', 'icons', 'jquery-ui', 'lib', 'skin', 'ui'): | 
| +    yield os.path.join(baseDir, file) | 
| +  if params['devenv']: | 
| +    yield os.path.join(baseDir, 'qunit') | 
| +  for file in os.listdir(baseDir): | 
| +    if file.endswith('.js') or file.endswith('.html') or file.endswith('.xml'): | 
| +      yield os.path.join(baseDir, file) | 
|  | 
| -def setUpdateURL(updateName, zip, dir, fileName, fileData): | 
| -  if fileName == 'manifest.json': | 
| -    data = json.loads(fileData) | 
| -    data['update_url'] = 'https://adblockplus.org/devbuilds/%s/updates.xml' % updateName | 
| -    return json.dumps(data, sort_keys=True, indent=2) | 
| -  return fileData | 
| +def createManifest(params): | 
| +  env = jinja2.Environment(loader=jinja2.FileSystemLoader(buildtools.__path__[0])) | 
| +  env.filters.update({'json': json.dumps}) | 
| +  template = env.get_template('manifest.json.tmpl') | 
| +  templateData = dict(params) | 
|  | 
| -def setExperimentalSettings(zip, dir, fileName, fileData): | 
| -  if fileName == 'manifest.json': | 
| -    data = json.loads(fileData) | 
| -    data['permissions'] += ['experimental'] | 
| -    data['name'] += ' experimental build' | 
| -    return json.dumps(data, sort_keys=True, indent=2) | 
| -  return fileData | 
| +  baseDir = templateData['baseDir'] | 
| +  metadata = templateData['metadata'] | 
|  | 
| -def addBuildNumber(revision, zip, dir, fileName, fileData): | 
| -  if fileName == 'manifest.json': | 
| -    if len(revision) > 0: | 
| -      data = json.loads(fileData) | 
| -      while data['version'].count('.') < 2: | 
| -        data['version'] += '.0' | 
| -      data['version'] += '.' + revision | 
| -      return json.dumps(data, sort_keys=True, indent=2) | 
| -  return fileData | 
| +  if metadata.has_option('general', 'pageAction'): | 
| +    icon, popup = re.split(r'\s+', metadata.get('general', 'pageAction'), 1) | 
| +    templateData['pageAction'] = {'icon': icon, 'popup': popup} | 
|  | 
| -def mergeContentScripts(zip, dir, fileName, fileData): | 
| -  if fileName == 'manifest.json': | 
| -    data = json.loads(fileData) | 
| -    if 'content_scripts' in data: | 
| -      scriptIndex = 1 | 
| -      for contentScript in data['content_scripts']: | 
| -        if 'js' in contentScript: | 
| -          scriptData = '' | 
| -          for scriptFile in contentScript['js']: | 
| -            parts = [dir] + scriptFile.split('/') | 
| -            scriptPath = os.path.join(*parts) | 
| -            handle = open(scriptPath, 'rb') | 
| -            scriptData += handle.read() | 
| -            handle.close() | 
| -          contentScript['js'] = ['contentScript' + str(scriptIndex) + '.js'] | 
| -          zip.writestr('contentScript' + str(scriptIndex) + '.js', scriptData) | 
| -          scriptIndex += 1 | 
| -    return json.dumps(data, sort_keys=True, indent=2) | 
| -  return fileData | 
| +  if metadata.has_option('general', 'icons'): | 
| +    icons = {} | 
| +    iconsDir = baseDir | 
| +    for dir in metadata.get('general', 'icons').split('/')[0:-1]: | 
| +      iconsDir = os.path.join(iconsDir, dir) | 
|  | 
| -def addToZip(zip, filters, dir, baseName): | 
| -  for file in os.listdir(dir): | 
| -    filelc = file.lower() | 
| -    if (file.startswith('.') or | 
| -        file == 'buildtools' or file == 'qunit' or file == 'metadata' or | 
| -        file == 'store.description' or | 
| -        filelc.endswith('.py') or filelc.endswith('.pyc') or | 
| -        filelc.endswith('.crx') or filelc.endswith('.zip') or | 
| -        filelc.endswith('.sh') or filelc.endswith('.bat') or | 
| -        filelc.endswith('.txt')): | 
| -      # skip special files, scripts, existing archives | 
| -      continue | 
| -    if file.startswith('include.'): | 
| -      # skip includes, they will be added by other means | 
| -      continue | 
| +    prefix, suffix = metadata.get('general', 'icons').split('/')[-1].split('?', 1) | 
| +    for file in os.listdir(iconsDir): | 
| +      path = os.path.join(iconsDir, file) | 
| +      if os.path.isfile(path) and file.startswith(prefix) and file.endswith(suffix): | 
| +        size = file[len(prefix):-len(suffix)] | 
| +        if not re.search(r'\D', size): | 
| +          icons[size] = os.path.relpath(path, baseDir).replace('\\', '/') | 
|  | 
| -    filePath = os.path.join(dir, file) | 
| -    if os.path.isdir(filePath): | 
| -      addToZip(zip, filters, filePath, baseName + file + '/') | 
| +    templateData['icons'] = icons | 
| + | 
| +  if metadata.has_option('general', 'permissions'): | 
| +    templateData['permissions'] = re.split(r'\s+', metadata.get('general', 'permissions')) | 
| +    if params['experimentalAPI']: | 
| +      templateData['permissions'].append('experimental') | 
| + | 
| +  if metadata.has_option('general', 'backgroundScripts'): | 
| +    templateData['backgroundScripts'] = re.split(r'\s+', metadata.get('general', 'backgroundScripts')) | 
| + | 
| +  if metadata.has_option('general', 'webAccessible'): | 
| +    templateData['webAccessible'] = re.split(r'\s+', metadata.get('general', 'webAccessible')) | 
| + | 
| +  if metadata.has_section('contentScripts'): | 
| +    contentScripts = [] | 
| +    for run_at, scripts in metadata.items('contentScripts'): | 
| +      contentScripts.append({ | 
| +        'matches': ['http://*/*', 'https://*/*'], | 
| +        'js': re.split(r'\s+', scripts), | 
| +        'run_at': run_at, | 
| +        'all_frames': True, | 
| +      }) | 
| +    templateData['contentScripts'] = contentScripts | 
| + | 
| +  manifest = template.render(templateData) | 
| + | 
| +  # Normalize JSON structure | 
| +  licenseComment = re.compile(r'/\*.*?\*/', re.S) | 
| +  data = json.loads(re.sub(licenseComment, '', manifest, 1)) | 
| +  if '_dummy' in data: | 
| +    del data['_dummy'] | 
| +  manifest = json.dumps(data, sort_keys=True, indent=2) | 
| + | 
| +  return manifest.encode('utf-8') | 
| + | 
| +def readFile(params, files, path): | 
| +  ignoredFiles = getIgnoredFiles(params) | 
| +  if os.path.isdir(path): | 
| +    for file in os.listdir(path): | 
| +      if file in ignoredFiles: | 
| +        continue | 
| +      readFile(params, files, os.path.join(path, file)) | 
| +  else: | 
| +    file = open(path, 'rb') | 
| +    data = file.read() | 
| +    file.close() | 
| + | 
| +    name = os.path.relpath(path, params['baseDir']).replace('\\', '/') | 
| +    files[name] = data | 
| + | 
| +def convertJS(params, files): | 
| +  baseDir = params['baseDir'] | 
| +  hydraDir = os.path.join(baseDir, 'jshydra') | 
| +  sys.path.append(hydraDir) | 
| +  try: | 
| +    if 'abp_rewrite' in sys.modules: | 
| +      import abp_rewrite | 
| +      reload(abp_rewrite.utils) | 
| +      reload(abp_rewrite) | 
| else: | 
| -      handle = open(filePath, 'rb') | 
| -      fileData = handle.read() | 
| -      handle.close() | 
| +      import abp_rewrite | 
|  | 
| -      for filter in filters: | 
| -        fileData = filter(zip, dir, baseName + file, fileData) | 
| -      zip.writestr(baseName + file, fileData) | 
| +    for file, sources in params['metadata'].items('convert_js'): | 
| +      dirsep = file.find('/') | 
| +      if dirsep >= 0: | 
| +        # Not a top-level file, make sure it is inside an included director | 
| +        dirname = file[0:dirsep] | 
| +        if os.path.join(baseDir, dirname) not in getPackageFiles(params): | 
| +          continue | 
|  | 
| -def packDirectory(dir, filters): | 
| +      sourceFiles = re.split(r'\s+', sources) | 
| +      args = [] | 
| +      try: | 
| +        argsStart = sourceFiles.index('--arg') | 
| +        args = sourceFiles[argsStart + 1:] | 
| +        sourceFiles = sourceFiles[0:argsStart] | 
| +      except ValueError: | 
| +        pass | 
| + | 
| +      sourceFiles = map(lambda f: os.path.abspath(os.path.join(baseDir, f)), sourceFiles) | 
| +      files[file] = abp_rewrite.doRewrite(sourceFiles, args) | 
| +  finally: | 
| +    sys.path.remove(hydraDir) | 
| + | 
| +def packFiles(files): | 
| buffer = StringIO() | 
| zip = ZipFile(buffer, 'w', ZIP_DEFLATED) | 
| -  addToZip(zip, filters, dir, '') | 
| +  for file, data in files.iteritems(): | 
| +    zip.writestr(file, data) | 
| zip.close() | 
| return buffer.getvalue() | 
|  | 
| def signBinary(zipdata, keyFile): | 
| import M2Crypto | 
| if not os.path.exists(keyFile): | 
| M2Crypto.RSA.gen_key(1024, 65537, callback=lambda x: None).save_key(keyFile, cipher=None) | 
| key = M2Crypto.EVP.load_key(keyFile) | 
| @@ -138,42 +184,64 @@ def signBinary(zipdata, keyFile): | 
| key.sign_update(zipdata) | 
| return key.final() | 
|  | 
| def getPublicKey(keyFile): | 
| import M2Crypto | 
| return M2Crypto.EVP.load_key(keyFile).as_der() | 
|  | 
| def writePackage(outputFile, pubkey, signature, zipdata): | 
| -  file = open(outputFile, 'wb') | 
| +  if isinstance(outputFile, basestring): | 
| +    file = open(outputFile, 'wb') | 
| +  else: | 
| +    file = outputFile | 
| if pubkey != None and signature != None: | 
| file.write(struct.pack('<4sIII', 'Cr24', 2, len(pubkey), len(signature))) | 
| file.write(pubkey) | 
| file.write(signature) | 
| file.write(zipdata) | 
| -  file.close() | 
|  | 
| -def createBuild(baseDir, outFile=None, buildNum=None, releaseBuild=False, keyFile=None, experimentalAPI=False): | 
| +def createBuild(baseDir, outFile=None, buildNum=None, releaseBuild=False, keyFile=None, experimentalAPI=False, devenv=False): | 
| metadata = readMetadata(baseDir) | 
| -  version = readVersion(baseDir) | 
| if outFile == None: | 
| -    outFile = getDefaultFileName(baseDir, metadata, version, 'crx' if keyFile else 'zip') | 
| +    outFile = getDefaultFileName(baseDir, metadata, 'crx' if keyFile else 'zip') | 
|  | 
| -  filters = [] | 
| +  version = metadata.get('general', 'version') | 
| if not releaseBuild: | 
| if buildNum == None: | 
| buildNum = getBuildNum(baseDir) | 
| -    filters.append(lambda zip, dir, fileName, fileData: addBuildNumber(buildNum, zip, dir, fileName, fileData)) | 
| +    if len(buildNum) > 0: | 
| +      while version.count('.') < 2: | 
| +        version += '.0' | 
| +      version += '.' + buildNum | 
|  | 
| -    baseName = metadata.get('general', 'basename') | 
| -    updateName = baseName + '-experimental' if experimentalAPI else baseName | 
| -    filters.append(lambda zip, dir, fileName, fileData: setUpdateURL(updateName, zip, dir, fileName, fileData)) | 
| -    if experimentalAPI: | 
| -      filters.append(setExperimentalSettings) | 
| -  filters.append(mergeContentScripts) | 
| +  params = { | 
| +    'baseDir': baseDir, | 
| +    'releaseBuild': releaseBuild, | 
| +    'version': version, | 
| +    'experimentalAPI': experimentalAPI, | 
| +    'devenv': devenv, | 
| +    'metadata': metadata, | 
| +  } | 
|  | 
| -  zipdata = packDirectory(baseDir, filters) | 
| +  files = {} | 
| +  files['manifest.json'] = createManifest(params) | 
| +  for path in getPackageFiles(params): | 
| +    if os.path.exists(path): | 
| +      readFile(params, files, path) | 
| + | 
| +  if metadata.has_section('convert_js') and os.path.isdir(os.path.join(baseDir, 'jshydra')): | 
| +    convertJS(params, files) | 
| + | 
| +  zipdata = packFiles(files) | 
| signature = None | 
| pubkey = None | 
| if keyFile != None: | 
| signature = signBinary(zipdata, keyFile) | 
| pubkey = getPublicKey(keyFile) | 
| writePackage(outFile, pubkey, signature, zipdata) | 
| + | 
| +def createDevEnv(baseDir): | 
| +  fileBuffer = StringIO() | 
| +  createBuild(baseDir, outFile=fileBuffer, devenv=True, releaseBuild=True) | 
| +  zip = ZipFile(StringIO(fileBuffer.getvalue()), 'r') | 
| +  zip.extractall(os.path.join(baseDir, 'devenv')) | 
| +  zip.close() | 
|  |