Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: packagerChrome.py

Issue 9051052: Changes to Chrome build process (Closed)
Patch Set: Moved JSHydra dependency to build tools and added automatic devenv reloading Created Dec. 29, 2012, 8:14 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « manifest.json.tmpl ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # coding: utf-8 1 # coding: utf-8
2 2
3 # This file is part of the Adblock Plus build tools, 3 # This file is part of the Adblock Plus build tools,
4 # Copyright (C) 2006-2012 Eyeo GmbH 4 # Copyright (C) 2006-2012 Eyeo GmbH
5 # 5 #
6 # Adblock Plus is free software: you can redistribute it and/or modify 6 # Adblock Plus is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License version 3 as 7 # it under the terms of the GNU General Public License version 3 as
8 # published by the Free Software Foundation. 8 # published by the Free Software Foundation.
9 # 9 #
10 # Adblock Plus is distributed in the hope that it will be useful, 10 # Adblock Plus is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details. 13 # GNU General Public License for more details.
14 # 14 #
15 # You should have received a copy of the GNU General Public License 15 # You should have received a copy of the GNU General Public License
16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
17 17
18 import sys, os, subprocess, re, json, codecs, struct 18 import sys, os, subprocess, re, json, codecs, struct, jinja2, buildtools
19 from ConfigParser import SafeConfigParser 19 from ConfigParser import SafeConfigParser
20 from StringIO import StringIO 20 from StringIO import StringIO
21 from zipfile import ZipFile, ZIP_DEFLATED 21 from zipfile import ZipFile, ZIP_DEFLATED
22 22
23 defaultLocale = 'en_US' 23 defaultLocale = 'en_US'
24 24
25 def getDefaultFileName(baseDir, metadata, version, ext): 25 def getDefaultFileName(baseDir, metadata, ext):
26 return os.path.join(baseDir, '%s-%s.%s' % (metadata.get('general', 'basename') , version, ext)) 26 return os.path.join(baseDir, '%s-%s.%s' % (metadata.get('general', 'basename') , metadata.get('general', 'version'), ext))
27 27
28 def getMetadataPath(baseDir): 28 def getMetadataPath(baseDir):
29 return os.path.join(baseDir, 'metadata') 29 return os.path.join(baseDir, 'metadata')
30 30
31 def getBuildNum(baseDir): 31 def getBuildNum(baseDir):
32 try: 32 try:
33 (result, dummy) = subprocess.Popen(['hg', 'id', '-n'], stdout=subprocess.PIP E).communicate() 33 (result, dummy) = subprocess.Popen(['hg', 'id', '-n'], stdout=subprocess.PIP E).communicate()
34 return re.sub(r'\D', '', result) 34 return re.sub(r'\D', '', result)
35 except Exception: 35 except Exception:
36 return '0' 36 return '0'
37 37
38 def getIgnoredFiles(params):
39 return ['store.description']
40
38 def readMetadata(baseDir): 41 def readMetadata(baseDir):
39 metadata = SafeConfigParser() 42 metadata = SafeConfigParser()
40 metadata.optionxform = str 43 metadata.optionxform = str
41 file = codecs.open(getMetadataPath(baseDir), 'rb', encoding='utf-8') 44 file = codecs.open(getMetadataPath(baseDir), 'rb', encoding='utf-8')
42 metadata.readfp(file) 45 metadata.readfp(file)
43 file.close() 46 file.close()
44 return metadata 47 return metadata
45 48
46 def readVersion(baseDir): 49 def getPackageFiles(params):
47 file = open(os.path.join(baseDir, 'manifest.json')) 50 baseDir = params['baseDir']
48 data = json.load(file) 51 for file in ('_locales', 'icons', 'jquery-ui', 'lib', 'skin', 'ui'):
49 file.close() 52 yield os.path.join(baseDir, file)
50 return data['version'] 53 if params['devenv']:
54 yield os.path.join(baseDir, 'qunit')
55 for file in os.listdir(baseDir):
56 if file.endswith('.js') or file.endswith('.html') or file.endswith('.xml'):
57 yield os.path.join(baseDir, file)
51 58
52 def setUpdateURL(updateName, zip, dir, fileName, fileData): 59 def createManifest(params):
53 if fileName == 'manifest.json': 60 env = jinja2.Environment(loader=jinja2.FileSystemLoader(buildtools.__path__[0] ))
54 data = json.loads(fileData) 61 env.filters.update({'json': json.dumps})
55 data['update_url'] = 'https://adblockplus.org/devbuilds/%s/updates.xml' % up dateName 62 template = env.get_template('manifest.json.tmpl')
56 return json.dumps(data, sort_keys=True, indent=2) 63 templateData = dict(params)
57 return fileData
58 64
59 def setExperimentalSettings(zip, dir, fileName, fileData): 65 baseDir = templateData['baseDir']
60 if fileName == 'manifest.json': 66 metadata = templateData['metadata']
61 data = json.loads(fileData)
62 data['permissions'] += ['experimental']
63 data['name'] += ' experimental build'
64 return json.dumps(data, sort_keys=True, indent=2)
65 return fileData
66 67
67 def addBuildNumber(revision, zip, dir, fileName, fileData): 68 if metadata.has_option('general', 'pageAction'):
68 if fileName == 'manifest.json': 69 icon, popup = re.split(r'\s+', metadata.get('general', 'pageAction'), 1)
69 if len(revision) > 0: 70 templateData['pageAction'] = {'icon': icon, 'popup': popup}
70 data = json.loads(fileData)
71 while data['version'].count('.') < 2:
72 data['version'] += '.0'
73 data['version'] += '.' + revision
74 return json.dumps(data, sort_keys=True, indent=2)
75 return fileData
76 71
77 def mergeContentScripts(zip, dir, fileName, fileData): 72 if metadata.has_option('general', 'icons'):
78 if fileName == 'manifest.json': 73 icons = {}
79 data = json.loads(fileData) 74 iconsDir = baseDir
80 if 'content_scripts' in data: 75 for dir in metadata.get('general', 'icons').split('/')[0:-1]:
81 scriptIndex = 1 76 iconsDir = os.path.join(iconsDir, dir)
82 for contentScript in data['content_scripts']:
83 if 'js' in contentScript:
84 scriptData = ''
85 for scriptFile in contentScript['js']:
86 parts = [dir] + scriptFile.split('/')
87 scriptPath = os.path.join(*parts)
88 handle = open(scriptPath, 'rb')
89 scriptData += handle.read()
90 handle.close()
91 contentScript['js'] = ['contentScript' + str(scriptIndex) + '.js']
92 zip.writestr('contentScript' + str(scriptIndex) + '.js', scriptData)
93 scriptIndex += 1
94 return json.dumps(data, sort_keys=True, indent=2)
95 return fileData
96 77
97 def addToZip(zip, filters, dir, baseName): 78 prefix, suffix = metadata.get('general', 'icons').split('/')[-1].split('?', 1)
98 for file in os.listdir(dir): 79 for file in os.listdir(iconsDir):
99 filelc = file.lower() 80 path = os.path.join(iconsDir, file)
100 if (file.startswith('.') or 81 if os.path.isfile(path) and file.startswith(prefix) and file.endswith(suff ix):
101 file == 'buildtools' or file == 'qunit' or file == 'metadata' or 82 size = file[len(prefix):-len(suffix)]
Felix Dahlke 2013/01/11 12:24:16 How about using a regex here? if os.path.isfile(p
Wladimir Palant 2013/01/11 14:45:39 As soon as we add the required escaping (prefix an
Felix Dahlke 2013/01/11 14:47:55 Right, forgot about the escaping. I suppose it's b
102 file == 'store.description' or 83 if not re.search(r'\D', size):
103 filelc.endswith('.py') or filelc.endswith('.pyc') or 84 icons[size] = os.path.relpath(path, baseDir).replace('\\', '/')
104 filelc.endswith('.crx') or filelc.endswith('.zip') or
105 filelc.endswith('.sh') or filelc.endswith('.bat') or
106 filelc.endswith('.txt')):
107 # skip special files, scripts, existing archives
108 continue
109 if file.startswith('include.'):
110 # skip includes, they will be added by other means
111 continue
112 85
113 filePath = os.path.join(dir, file) 86 templateData['icons'] = icons
114 if os.path.isdir(filePath):
115 addToZip(zip, filters, filePath, baseName + file + '/')
116 else:
117 handle = open(filePath, 'rb')
118 fileData = handle.read()
119 handle.close()
120 87
121 for filter in filters: 88 if metadata.has_option('general', 'permissions'):
122 fileData = filter(zip, dir, baseName + file, fileData) 89 templateData['permissions'] = re.split(r'\s+', metadata.get('general', 'perm issions'))
123 zip.writestr(baseName + file, fileData) 90 if params['experimentalAPI']:
91 templateData['permissions'].append('experimental')
124 92
125 def packDirectory(dir, filters): 93 if metadata.has_option('general', 'backgroundScripts'):
94 templateData['backgroundScripts'] = re.split(r'\s+', metadata.get('general', 'backgroundScripts'))
95 if params['devenv']:
96 templateData['backgroundScripts'].append('devenvPoller__.js')
97
98 if metadata.has_option('general', 'webAccessible'):
99 templateData['webAccessible'] = re.split(r'\s+', metadata.get('general', 'we bAccessible'))
100
101 if metadata.has_section('contentScripts'):
102 contentScripts = []
103 for run_at, scripts in metadata.items('contentScripts'):
104 contentScripts.append({
105 'matches': ['http://*/*', 'https://*/*'],
106 'js': re.split(r'\s+', scripts),
107 'run_at': run_at,
108 'all_frames': True,
109 })
110 templateData['contentScripts'] = contentScripts
111
112 manifest = template.render(templateData)
113
114 # Normalize JSON structure
115 licenseComment = re.compile(r'/\*.*?\*/', re.S)
116 data = json.loads(re.sub(licenseComment, '', manifest, 1))
117 if '_dummy' in data:
118 del data['_dummy']
119 manifest = json.dumps(data, sort_keys=True, indent=2)
120
121 return manifest.encode('utf-8')
122
123 def createPoller(params):
124 env = jinja2.Environment(loader=jinja2.FileSystemLoader(buildtools.__path__[0] ))
125 env.filters.update({'json': json.dumps})
126 template = env.get_template('chromeDevenvPoller__.js.tmpl')
127 return template.render(params).encode('utf-8');
128
129 def readFile(params, files, path):
130 ignoredFiles = getIgnoredFiles(params)
131 if os.path.isdir(path):
132 for file in os.listdir(path):
133 if file in ignoredFiles:
134 continue
135 readFile(params, files, os.path.join(path, file))
136 else:
137 file = open(path, 'rb')
138 data = file.read()
139 file.close()
140
141 name = os.path.relpath(path, params['baseDir']).replace('\\', '/')
142 files[name] = data
143
144 def convertJS(params, files):
145 from jshydra.abp_rewrite import doRewrite
146 baseDir = params['baseDir']
147
148 for file, sources in params['metadata'].items('convert_js'):
149 dirsep = file.find('/')
150 if dirsep >= 0:
151 # Not a top-level file, make sure it is inside an included directory
152 dirname = file[0:dirsep]
153 if os.path.join(baseDir, dirname) not in getPackageFiles(params):
154 continue
155
156 sourceFiles = re.split(r'\s+', sources)
157 args = []
158 try:
159 argsStart = sourceFiles.index('--arg')
160 args = sourceFiles[argsStart + 1:]
161 sourceFiles = sourceFiles[0:argsStart]
162 except ValueError:
163 pass
164
165 sourceFiles = map(lambda f: os.path.abspath(os.path.join(baseDir, f)), sourc eFiles)
166 files[file] = doRewrite(sourceFiles, args)
167
168 def packFiles(files):
126 buffer = StringIO() 169 buffer = StringIO()
127 zip = ZipFile(buffer, 'w', ZIP_DEFLATED) 170 zip = ZipFile(buffer, 'w', ZIP_DEFLATED)
128 addToZip(zip, filters, dir, '') 171 for file, data in files.iteritems():
172 zip.writestr(file, data)
129 zip.close() 173 zip.close()
130 return buffer.getvalue() 174 return buffer.getvalue()
131 175
132 def signBinary(zipdata, keyFile): 176 def signBinary(zipdata, keyFile):
133 import M2Crypto 177 import M2Crypto
134 if not os.path.exists(keyFile): 178 if not os.path.exists(keyFile):
135 M2Crypto.RSA.gen_key(1024, 65537, callback=lambda x: None).save_key(keyFile, cipher=None) 179 M2Crypto.RSA.gen_key(1024, 65537, callback=lambda x: None).save_key(keyFile, cipher=None)
136 key = M2Crypto.EVP.load_key(keyFile) 180 key = M2Crypto.EVP.load_key(keyFile)
137 key.sign_init() 181 key.sign_init()
138 key.sign_update(zipdata) 182 key.sign_update(zipdata)
139 return key.final() 183 return key.final()
140 184
141 def getPublicKey(keyFile): 185 def getPublicKey(keyFile):
142 import M2Crypto 186 import M2Crypto
143 return M2Crypto.EVP.load_key(keyFile).as_der() 187 return M2Crypto.EVP.load_key(keyFile).as_der()
144 188
145 def writePackage(outputFile, pubkey, signature, zipdata): 189 def writePackage(outputFile, pubkey, signature, zipdata):
146 file = open(outputFile, 'wb') 190 if isinstance(outputFile, basestring):
191 file = open(outputFile, 'wb')
192 else:
193 file = outputFile
147 if pubkey != None and signature != None: 194 if pubkey != None and signature != None:
148 file.write(struct.pack('<4sIII', 'Cr24', 2, len(pubkey), len(signature))) 195 file.write(struct.pack('<4sIII', 'Cr24', 2, len(pubkey), len(signature)))
149 file.write(pubkey) 196 file.write(pubkey)
150 file.write(signature) 197 file.write(signature)
151 file.write(zipdata) 198 file.write(zipdata)
152 file.close()
153 199
154 def createBuild(baseDir, outFile=None, buildNum=None, releaseBuild=False, keyFil e=None, experimentalAPI=False): 200 def createBuild(baseDir, outFile=None, buildNum=None, releaseBuild=False, keyFil e=None, experimentalAPI=False, devenv=False):
155 metadata = readMetadata(baseDir) 201 metadata = readMetadata(baseDir)
156 version = readVersion(baseDir)
157 if outFile == None: 202 if outFile == None:
158 outFile = getDefaultFileName(baseDir, metadata, version, 'crx' if keyFile el se 'zip') 203 outFile = getDefaultFileName(baseDir, metadata, 'crx' if keyFile else 'zip')
159 204
160 filters = [] 205 version = metadata.get('general', 'version')
161 if not releaseBuild: 206 if not releaseBuild:
162 if buildNum == None: 207 if buildNum == None:
163 buildNum = getBuildNum(baseDir) 208 buildNum = getBuildNum(baseDir)
164 filters.append(lambda zip, dir, fileName, fileData: addBuildNumber(buildNum, zip, dir, fileName, fileData)) 209 if len(buildNum) > 0:
210 while version.count('.') < 2:
211 version += '.0'
212 version += '.' + buildNum
165 213
166 baseName = metadata.get('general', 'basename') 214 params = {
167 updateName = baseName + '-experimental' if experimentalAPI else baseName 215 'baseDir': baseDir,
168 filters.append(lambda zip, dir, fileName, fileData: setUpdateURL(updateName, zip, dir, fileName, fileData)) 216 'releaseBuild': releaseBuild,
169 if experimentalAPI: 217 'version': version,
170 filters.append(setExperimentalSettings) 218 'experimentalAPI': experimentalAPI,
171 filters.append(mergeContentScripts) 219 'devenv': devenv,
220 'metadata': metadata,
221 }
172 222
173 zipdata = packDirectory(baseDir, filters) 223 files = {}
224 files['manifest.json'] = createManifest(params)
225 for path in getPackageFiles(params):
226 if os.path.exists(path):
227 readFile(params, files, path)
228
229 if metadata.has_section('convert_js'):
230 convertJS(params, files)
231
232 if devenv:
233 files['devenvPoller__.js'] = createPoller(params)
234
235 zipdata = packFiles(files)
174 signature = None 236 signature = None
175 pubkey = None 237 pubkey = None
176 if keyFile != None: 238 if keyFile != None:
177 signature = signBinary(zipdata, keyFile) 239 signature = signBinary(zipdata, keyFile)
178 pubkey = getPublicKey(keyFile) 240 pubkey = getPublicKey(keyFile)
179 writePackage(outFile, pubkey, signature, zipdata) 241 writePackage(outFile, pubkey, signature, zipdata)
242
243 def createDevEnv(baseDir):
244 fileBuffer = StringIO()
245 createBuild(baseDir, outFile=fileBuffer, devenv=True, releaseBuild=True)
246 zip = ZipFile(StringIO(fileBuffer.getvalue()), 'r')
247 zip.extractall(os.path.join(baseDir, 'devenv'))
248 zip.close()
249
250 print 'Development environment created, waiting for connections from active ex tensions...'
251 metadata = readMetadata(baseDir)
252 connections = [0]
253
254 import SocketServer, time, thread
255
256 class ConnectionHandler(SocketServer.BaseRequestHandler):
257 def handle(self):
258 connections[0] += 1
259 self.request.sendall('HTTP/1.0 OK\nConnection: close\n\n%s' % metadata.get ('general', 'basename'))
260
261 server = SocketServer.TCPServer(('localhost', 43816), ConnectionHandler)
262
263 def shutdown_server(server):
264 time.sleep(10)
265 server.shutdown()
266 thread.start_new_thread(shutdown_server, (server,))
267 server.serve_forever()
268
269 if connections[0] == 0:
270 print 'Warning: No incoming connections, extension probably not active in th e browser yet'
271 else:
272 print 'Handled %i connection(s)' % connections[0]
OLDNEW
« no previous file with comments | « manifest.json.tmpl ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld