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

Delta Between Two Patch Sets: ensure_dependencies.py

Issue 5168251361296384: Issue 170 - Replacing Mercurial subrepositories (Closed)
Left Patch Set: Renamed script Created Sept. 3, 2014, 9:55 p.m.
Right Patch Set: Addressed remaining comments Created Sept. 9, 2014, 3:46 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # coding: utf-8 2 # coding: utf-8
Sebastian Noack 2014/09/04 10:07:30 I don't find any non-ascii characters below. Also
Wladimir Palant 2014/09/04 13:32:12 We specify this in all Python files, UTF-8 is the
Felix Dahlke 2014/09/05 08:55:53 What PEP-8 says, specifically, is this: "Files us
Sebastian Noack 2014/09/05 09:29:30 Keep reading ;P "... non-default encodings should
Felix Dahlke 2014/09/05 09:55:01 You left the first part out: "In the standard libr
3 3
4 # This file is part of the Adblock Plus build tools, 4 # This file is part of the Adblock Plus build tools,
5 # Copyright (C) 2006-2014 Eyeo GmbH 5 # Copyright (C) 2006-2014 Eyeo GmbH
6 # 6 #
7 # Adblock Plus is free software: you can redistribute it and/or modify 7 # Adblock Plus is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License version 3 as 8 # it under the terms of the GNU General Public License version 3 as
9 # published by the Free Software Foundation. 9 # published by the Free Software Foundation.
10 # 10 #
11 # Adblock Plus is distributed in the hope that it will be useful, 11 # Adblock Plus is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details. 14 # GNU General Public License for more details.
15 # 15 #
16 # You should have received a copy of the GNU General Public License 16 # You should have received a copy of the GNU General Public License
17 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 17 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
18 18
19 import sys 19 import sys
20 import os 20 import os
21 import posixpath
21 import re 22 import re
22 import io 23 import io
23 import errno 24 import errno
24 import warnings 25 import logging
25 import subprocess 26 import subprocess
26 import urlparse 27 import urlparse
27 from collections import OrderedDict 28 from collections import OrderedDict
29
30 USAGE = """
31 A dependencies file should look like this:
32
33 # VCS-specific root URLs for the repositories
34 _root = hg:https://hg.adblockplus.org/ git:https://github.com/adblockplus/
35 # File to update this script from (optional)
36 _self = buildtools/ensure_dependencies.py
37 # Check out elemhidehelper repository into extensions/elemhidehelper directory
38 # at tag "1.2".
39 extensions/elemhidehelper = elemhidehelper 1.2
40 # Check out buildtools repository into buildtools directory at VCS-specific
41 # revision IDs.
42 buildtools = buildtools hg:016d16f7137b git:f3f8692f82e5
43 """
28 44
29 class Mercurial(): 45 class Mercurial():
30 def istype(self, repodir): 46 def istype(self, repodir):
31 return os.path.exists(os.path.join(repodir, ".hg")) 47 return os.path.exists(os.path.join(repodir, ".hg"))
32 48
33 def clone(self, source, target): 49 def clone(self, source, target):
34 if not source.endswith("/"): 50 if not source.endswith("/"):
35 source += "/" 51 source += "/"
36 subprocess.check_call(["hg", "clone", "--quiet", "--noupdate", source, targe t]) 52 subprocess.check_call(["hg", "clone", "--quiet", "--noupdate", source, targe t])
37 53
38 def get_revision_id(self, repo, rev=None): 54 def get_revision_id(self, repo, rev=None):
39 command = ["hg", "id", "--repository", repo, "--id"] 55 command = ["hg", "id", "--repository", repo, "--id"]
40 if rev: 56 if rev:
41 command.extend(["--rev", rev]) 57 command.extend(["--rev", rev])
42 58
43 # Ignore stderr output and return code here: if revision lookup failed we 59 # Ignore stderr output and return code here: if revision lookup failed we
44 # should simply return an empty string. 60 # should simply return an empty string.
45 result, dummy = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sub process.PIPE).communicate() 61 result = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess .PIPE).communicate()[0]
46 return result.strip() 62 return result.strip()
47 63
48 def pull(self, repo): 64 def pull(self, repo):
49 subprocess.check_call(["hg", "pull", "--repository", repo, "--quiet"]) 65 subprocess.check_call(["hg", "pull", "--repository", repo, "--quiet"])
50 66
51 def update(self, repo, rev): 67 def update(self, repo, rev):
52 subprocess.check_call(["hg", "update", "--repository", repo, "--quiet", "--c heck", "--rev", rev]) 68 subprocess.check_call(["hg", "update", "--repository", repo, "--quiet", "--c heck", "--rev", rev])
53 69
54 class Git(): 70 class Git():
55 def istype(self, repodir): 71 def istype(self, repodir):
(...skipping 15 matching lines...) Expand all
71 def update(self, repo, rev): 87 def update(self, repo, rev):
72 subprocess.check_call(["git", "-C", repo, "checkout", "--quiet", rev]) 88 subprocess.check_call(["git", "-C", repo, "checkout", "--quiet", rev])
73 89
74 repo_types = { 90 repo_types = {
75 "hg": Mercurial(), 91 "hg": Mercurial(),
76 "git": Git(), 92 "git": Git(),
77 } 93 }
78 94
79 def parse_spec(path, line): 95 def parse_spec(path, line):
80 if "=" not in line: 96 if "=" not in line:
81 warnings.warn("Invalid line in file %s: %s" % (path, line)) 97 logging.warning("Invalid line in file %s: %s" % (path, line))
82 return None, None 98 return None, None
83 99
84 key, value = line.split("=", 1) 100 key, value = line.split("=", 1)
85 key = key.strip() 101 key = key.strip()
86 items = value.split() 102 items = value.split()
87 if not len(items): 103 if not len(items):
88 warnings.warn("No value specified for key %s in file %s" % (key, path)) 104 logging.warning("No value specified for key %s in file %s" % (key, path))
89 return key, None 105 return key, None
90 106
91 result = OrderedDict() 107 result = OrderedDict()
92 if not key.startswith("_"): 108 if not key.startswith("_"):
93 result["_source"] = items.pop(0) 109 result["_source"] = items.pop(0)
94 110
95 for item in items: 111 for item in items:
96 if ":" in item: 112 if ":" in item:
97 type, value = item.split(":", 1) 113 type, value = item.split(":", 1)
98 else: 114 else:
99 type, value = ("*", item) 115 type, value = ("*", item)
100 if type in result: 116 if type in result:
101 warnings.warn("Ignoring duplicate value for type %s (key %s in file %s)" % (type, key, path)) 117 logging.warning("Ignoring duplicate value for type %s (key %s in file %s)" % (type, key, path))
102 else: 118 else:
103 result[type] = value 119 result[type] = value
104 return key, result 120 return key, result
105 121
106 def read_deps(repodir): 122 def read_deps(repodir):
107 result = {} 123 result = {}
108 deps_path = os.path.join(repodir, "dependencies") 124 deps_path = os.path.join(repodir, "dependencies")
109 try: 125 try:
110 with io.open(deps_path, "rt", encoding="utf-8") as handle: 126 with io.open(deps_path, "rt", encoding="utf-8") as handle:
111 for line in handle: 127 for line in handle:
112 # Remove comments and whitespace 128 # Remove comments and whitespace
113 line = re.sub(r"#.*", "", line).strip() 129 line = re.sub(r"#.*", "", line).strip()
114 if not line: 130 if not line:
115 continue 131 continue
116 132
117 key, spec = parse_spec(deps_path, line) 133 key, spec = parse_spec(deps_path, line)
118 if spec: 134 if spec:
119 result[key] = spec 135 result[key] = spec
120 return result 136 return result
121 except IOError, e: 137 except IOError, e:
122 if e.errno != errno.ENOENT: 138 if e.errno != errno.ENOENT:
123 raise 139 raise
124 return None 140 return None
125 141
126 def safe_join(path, subpath): 142 def safe_join(path, subpath):
127 return os.path.join(path, *[f for f in subpath.split("/") if f not in (os.curd ir, os.pardir)]) 143 # This has been inspired by Flask's safe_join() function
144 forbidden = set([os.sep, os.altsep]) - set([posixpath.sep, None])
145 if any(sep in subpath for sep in forbidden):
146 raise Exception("Illegal directory separator in dependency path %s" % subpat h)
147
148 normpath = posixpath.normpath(subpath)
149 if posixpath.isabs(normpath):
150 raise Exception("Dependency path %s cannot be absolute" % subpath)
151 if normpath == posixpath.pardir or normpath.startswith(posixpath.pardir + posi xpath.sep):
152 raise Exception("Dependency path %s has to be inside the repository" % subpa th)
153 return os.path.join(path, *normpath.split(posixpath.sep))
128 154
129 def get_repo_type(repo): 155 def get_repo_type(repo):
130 for name, repotype in repo_types.iteritems(): 156 for name, repotype in repo_types.iteritems():
131 if repotype.istype(repo): 157 if repotype.istype(repo):
132 return name 158 return name
133 return None 159 return None
134 160
135 def ensure_repo(parentrepo, target, roots, sourcename): 161 def ensure_repo(parentrepo, target, roots, sourcename):
136 if os.path.exists(target): 162 if os.path.exists(target):
137 return 163 return
138 164
139 parenttype = get_repo_type(parentrepo) 165 parenttype = get_repo_type(parentrepo)
140 type = None 166 type = None
141 for key in roots: 167 for key in roots:
142 if key == parenttype or (key in repo_types and type is None): 168 if key == parenttype or (key in repo_types and type is None):
143 type = key 169 type = key
144 if type is None: 170 if type is None:
145 raise Exception("No valid source found to create %s" % target) 171 raise Exception("No valid source found to create %s" % target)
146 172
147 url = urlparse.urljoin(roots[type], sourcename) 173 url = urlparse.urljoin(roots[type], sourcename)
148 print "Cloning repository %s into %s" % (url, target) 174 logging.info("Cloning repository %s into %s" % (url, target))
149 repo_types[type].clone(url, target) 175 repo_types[type].clone(url, target)
150 176
151 def update_repo(target, revisions): 177 def update_repo(target, revisions):
152 type = get_repo_type(target) 178 type = get_repo_type(target)
153 if type is None: 179 if type is None:
154 warnings.warn("Type of repository %s unknown, skipping update" % target) 180 logging.warning("Type of repository %s unknown, skipping update" % target)
155 return 181 return
156 182
157 if type in revisions: 183 if type in revisions:
158 revision = revisions[type] 184 revision = revisions[type]
159 elif "*" in revisions: 185 elif "*" in revisions:
160 revision = revisions["*"] 186 revision = revisions["*"]
161 else: 187 else:
162 warnings.warn("No revision specified for repository %s (type %s), skipping u pdate" % (target, type)) 188 logging.warning("No revision specified for repository %s (type %s), skipping update" % (target, type))
163 return 189 return
164 190
165 resolved_revision = repo_types[type].get_revision_id(target, revision) 191 resolved_revision = repo_types[type].get_revision_id(target, revision)
166 if not resolved_revision: 192 if not resolved_revision:
167 print "Revision %s is unknown, downloading remote changes" % revision 193 logging.info("Revision %s is unknown, downloading remote changes" % revision )
168 repo_types[type].pull(target) 194 repo_types[type].pull(target)
169 resolved_revision = repo_types[type].get_revision_id(target, revision) 195 resolved_revision = repo_types[type].get_revision_id(target, revision)
170 if not resolved_revision: 196 if not resolved_revision:
171 raise Exception("Failed to resolve revision %s" % revision) 197 raise Exception("Failed to resolve revision %s" % revision)
172 198
173 current_revision = repo_types[type].get_revision_id(target) 199 current_revision = repo_types[type].get_revision_id(target)
174 if resolved_revision != current_revision: 200 if resolved_revision != current_revision:
175 print "Updating repository %s to revision %s" % (target, resolved_revision) 201 logging.info("Updating repository %s to revision %s" % (target, resolved_rev ision))
176 repo_types[type].update(target, resolved_revision) 202 repo_types[type].update(target, resolved_revision)
177 203
178 def resolve_deps(repodir, level=0): 204 def resolve_deps(repodir, level=0, self_update=True, overrideroots=None, skipdep endencies=set()):
179 config = read_deps(repodir) 205 config = read_deps(repodir)
180 if config is None: 206 if config is None:
181 if level == 0: 207 if level == 0:
182 warnings.warn("No dependencies file in directory %s, nothing to do..." % r epodir) 208 logging.warning("No dependencies file in directory %s, nothing to do...\n% s" % (repodir, USAGE))
183 return 209 return
184 if level >= 10: 210 if level >= 10:
185 warnings.warn("Too much subrepository nesting, ignoring %s" % repo) 211 logging.warning("Too much subrepository nesting, ignoring %s" % repo)
186 212
187 for dir in config: 213 if overrideroots is not None:
188 if dir.startswith("_"): 214 config["_root"] = overrideroots
215
216 for dir, revisions in config.iteritems():
217 if dir.startswith("_") or revisions["_source"] in skipdependencies:
189 continue 218 continue
190 target = safe_join(repodir, dir) 219 target = safe_join(repodir, dir)
191 ensure_repo(repodir, target, config.get("_root", {}), config[dir]["_source"] ) 220 ensure_repo(repodir, target, config.get("_root", {}), revisions["_source"])
192 update_repo(target, config[dir]) 221 update_repo(target, revisions)
193 resolve_deps(target, level + 1) 222 resolve_deps(target, level + 1, self_update=False, overrideroots=overrideroo ts, skipdependencies=skipdependencies)
194 223
195 if level == 0 and "_self" in config and "*" in config["_self"]: 224 if self_update and "_self" in config and "*" in config["_self"]:
196 source = safe_join(repodir, config["_self"]["*"]) 225 source = safe_join(repodir, config["_self"]["*"])
197 try: 226 try:
198 with io.open(source, "rb") as handle: 227 with io.open(source, "rb") as handle:
199 sourcedata = handle.read() 228 sourcedata = handle.read()
200 except IOError, e: 229 except IOError, e:
201 if e.errno != errno.ENOENT: 230 if e.errno != errno.ENOENT:
202 raise 231 raise
203 warnings.warn("File %s doesn't exist, skipping self-update" % source) 232 logging.warning("File %s doesn't exist, skipping self-update" % source)
204 return 233 return
205 234
206 target = __file__ 235 target = __file__
207 with io.open(target, "rb") as handle: 236 with io.open(target, "rb") as handle:
208 targetdata = handle.read() 237 targetdata = handle.read()
209 238
210 if sourcedata != targetdata: 239 if sourcedata != targetdata:
211 print "Updating %s from %s, don't forget to commit" % (source, target) 240 logging.info("Updating %s from %s, don't forget to commit" % (source, targ et))
212 with io.open(target, "wb") as handle: 241 with io.open(target, "wb") as handle:
213 handle.write(sourcedata) 242 handle.write(sourcedata)
214 print "Restarting %s" % target
215 if __name__ == "__main__": 243 if __name__ == "__main__":
244 logging.info("Restarting %s" % target)
216 os.execv(sys.executable, [sys.executable, target] + sys.argv[1:]) 245 os.execv(sys.executable, [sys.executable, target] + sys.argv[1:])
Sebastian Noack 2014/09/04 10:07:30 I think you have to call os.exit(0) afterwards. Ot
Sebastian Noack 2014/09/04 13:50:17 I just realized that you are using os.execv(), whi
217 else: 246 else:
218 import importlib 247 logging.warning("Cannot restart %s automatically, please rerun" % target )
Sebastian Noack 2014/09/04 10:07:30 Is there a case where this script is actually impo
Wladimir Palant 2014/09/04 13:32:12 build.py will import it as module. And - no, it sh
Sebastian Noack 2014/09/04 13:44:52 I think you didn't get it. When buildtools calls r
Wladimir Palant 2014/09/04 14:34:45 I did get it, but I was rather referring to the sc
Wladimir Palant 2014/09/04 15:55:38 Actually, this doesn't seem to be that much of an
Sebastian Noack 2014/09/04 16:22:00 That is correct. Though this might be a footgun. I
219 me = importlib.import_module(__name__)
220 reload(me)
221 me.resolve_deps(repodir, level)
222
223 def showwarning(message, category, filename, lineno, file=sys.stderr, line=None) :
224 print >>file, "%s:%i: %s" % (filename, lineno, message)
225 248
226 if __name__ == "__main__": 249 if __name__ == "__main__":
227 warnings.showwarning = showwarning 250 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
228
229 repos = sys.argv[1:] 251 repos = sys.argv[1:]
230 if not len(repos): 252 if not len(repos):
231 repos = [os.getcwd()] 253 repos = [os.getcwd()]
232 for repo in repos: 254 for repo in repos:
233 resolve_deps(repo) 255 resolve_deps(repo)
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld