##// END OF EJS Templates
git: skeleton of a new extension to _directly_ operate on git repos...
Augie Fackler -
r44961:ad718271 default
parent child Browse files
Show More
@@ -0,0 +1,39 b''
1 Octopus Merge Support
2 =====================
3
4 This will be moderately complicated, as we'll need to synthesize phony
5 changeset entries to explode the octopus into "revisions" that only
6 have two parents each. For today, we can probably just do something like
7
8 aaaaaaaaaaaaaaaaaaXX{20 bytes of exploded node's hex sha}
9
10 where XX is a counter (so we could have as many as 255 parents in a
11 git commit - more than I think we'd ever see.) That means that we can
12 install some check in this extension to disallow checking out or
13 otherwise interacting with the `aaaaaaaaaaaaaaaaaa` revisions.
14
15
16 Interface Creation
17 ====================
18
19 We at least need an interface definition for `changelog` in core that
20 this extension can satisfy, and again for `basicstore`.
21
22
23 Reason About Locking
24 ====================
25
26 We should spend some time thinking hard about locking, especially on
27 .git/index etc. We're probably adequately locking the _git_
28 repository, but may not have enough locking correctness in places
29 where hg does locking that git isn't aware of (notably the working
30 copy, which I believe Git does not lock.)
31
32 Clean up requirements
33 =====================
34
35 Right now (for historical reasons, mainly) hgext.git uses a
36 .hg/this-is-git file to detect repositories that should be treated as
37 git. We should look in the .hg/requires for the "git" requirement
38 instead (we already set this requirement, so it's mostly keying off
39 that instead of using an empty file.)
@@ -0,0 +1,259 b''
1 """grant Mercurial the ability to operate on Git repositories. (EXPERIMENTAL)
2
3 This is currently super experimental. It probably will consume your
4 firstborn a la Rumpelstiltskin, etc.
5 """
6
7 from __future__ import absolute_import
8
9 import os
10
11 import pygit2
12
13 from mercurial.i18n import _
14
15 from mercurial import (
16 commands,
17 error,
18 extensions,
19 localrepo,
20 pycompat,
21 store,
22 util,
23 )
24
25 from . import (
26 dirstate,
27 gitlog,
28 gitutil,
29 index,
30 )
31
32
33 # TODO: extract an interface for this in core
34 class gitstore(object): # store.basicstore):
35 def __init__(self, path, vfstype):
36 self.vfs = vfstype(path)
37 self.path = self.vfs.base
38 self.createmode = store._calcmode(self.vfs)
39 # above lines should go away in favor of:
40 # super(gitstore, self).__init__(path, vfstype)
41
42 self.git = pygit2.Repository(
43 os.path.normpath(os.path.join(path, b'..', b'.git'))
44 )
45 self._progress_factory = lambda *args, **kwargs: None
46
47 @util.propertycache
48 def _db(self):
49 # We lazy-create the database because we want to thread a
50 # progress callback down to the indexing process if it's
51 # required, and we don't have a ui handle in makestore().
52 return index.get_index(self.git, self._progress_factory)
53
54 def join(self, f):
55 """Fake store.join method for git repositories.
56
57 For the most part, store.join is used for @storecache
58 decorators to invalidate caches when various files
59 change. We'll map the ones we care about, and ignore the rest.
60 """
61 if f in (b'00changelog.i', b'00manifest.i'):
62 # This is close enough: in order for the changelog cache
63 # to be invalidated, HEAD will have to change.
64 return os.path.join(self.path, b'HEAD')
65 elif f == b'lock':
66 # TODO: we probably want to map this to a git lock, I
67 # suspect index.lock. We should figure out what the
68 # most-alike file is in git-land. For now we're risking
69 # bad concurrency errors if another git client is used.
70 return os.path.join(self.path, b'hgit-bogus-lock')
71 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
72 return os.path.join(self.path, b'..', b'.hg', f)
73 raise NotImplementedError(b'Need to pick file for %s.' % f)
74
75 def changelog(self, trypending):
76 # TODO we don't have a plan for trypending in hg's git support yet
77 return gitlog.changelog(self.git, self._db)
78
79 def manifestlog(self, repo, storenarrowmatch):
80 # TODO handle storenarrowmatch and figure out if we need the repo arg
81 return gitlog.manifestlog(self.git, self._db)
82
83 def invalidatecaches(self):
84 pass
85
86 def write(self, tr=None):
87 # normally this handles things like fncache writes, which we don't have
88 pass
89
90
91 def _makestore(orig, requirements, storebasepath, vfstype):
92 if os.path.exists(
93 os.path.join(storebasepath, b'this-is-git')
94 ) and os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
95 return gitstore(storebasepath, vfstype)
96 return orig(requirements, storebasepath, vfstype)
97
98
99 class gitfilestorage(object):
100 def file(self, path):
101 if path[0:1] == b'/':
102 path = path[1:]
103 return gitlog.filelog(self.store.git, self.store._db, path)
104
105
106 def _makefilestorage(orig, requirements, features, **kwargs):
107 store = kwargs['store']
108 if isinstance(store, gitstore):
109 return gitfilestorage
110 return orig(requirements, features, **kwargs)
111
112
113 def _setupdothg(ui, path):
114 dothg = os.path.join(path, b'.hg')
115 if os.path.exists(dothg):
116 ui.warn(_(b'git repo already initialized for hg\n'))
117 else:
118 os.mkdir(os.path.join(path, b'.hg'))
119 # TODO is it ok to extend .git/info/exclude like this?
120 with open(
121 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
122 ) as exclude:
123 exclude.write(b'\n.hg\n')
124 with open(os.path.join(dothg, b'this-is-git'), 'wb') as f:
125 pass
126 with open(os.path.join(dothg, b'requirements'), 'wb') as f:
127 f.write(b'git\n')
128
129
130 _BMS_PREFIX = 'refs/heads/'
131
132
133 class gitbmstore(object):
134 def __init__(self, gitrepo):
135 self.gitrepo = gitrepo
136
137 def __contains__(self, name):
138 return (
139 _BMS_PREFIX + pycompat.fsdecode(name)
140 ) in self.gitrepo.references
141
142 def __iter__(self):
143 for r in self.gitrepo.listall_references():
144 if r.startswith(_BMS_PREFIX):
145 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
146
147 def __getitem__(self, k):
148 return (
149 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
150 .peel()
151 .id.raw
152 )
153
154 def get(self, k, default=None):
155 try:
156 if k in self:
157 return self[k]
158 return default
159 except pygit2.InvalidSpecError:
160 return default
161
162 @property
163 def active(self):
164 h = self.gitrepo.references['HEAD']
165 if not isinstance(h.target, str) or not h.target.startswith(
166 _BMS_PREFIX
167 ):
168 return None
169 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
170
171 @active.setter
172 def active(self, mark):
173 raise NotImplementedError
174
175 def names(self, node):
176 r = []
177 for ref in self.gitrepo.listall_references():
178 if not ref.startswith(_BMS_PREFIX):
179 continue
180 if self.gitrepo.references[ref].peel().id.raw != node:
181 continue
182 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
183 return r
184
185 # Cleanup opportunity: this is *identical* to core's bookmarks store.
186 def expandname(self, bname):
187 if bname == b'.':
188 if self.active:
189 return self.active
190 raise error.RepoLookupError(_(b"no active bookmark"))
191 return bname
192
193 def applychanges(self, repo, tr, changes):
194 """Apply a list of changes to bookmarks
195 """
196 # TODO: this should respect transactions, but that's going to
197 # require enlarging the gitbmstore to know how to do in-memory
198 # temporary writes and read those back prior to transaction
199 # finalization.
200 for name, node in changes:
201 if node is None:
202 self.gitrepo.references.delete(
203 _BMS_PREFIX + pycompat.fsdecode(name)
204 )
205 else:
206 self.gitrepo.references.create(
207 _BMS_PREFIX + pycompat.fsdecode(name),
208 gitutil.togitnode(node),
209 force=True,
210 )
211
212
213 def init(orig, ui, dest=b'.', **opts):
214 if opts.get('git', False):
215 path = os.path.abspath(dest)
216 # TODO: walk up looking for the git repo
217 _setupdothg(ui, path)
218 return 0
219 return orig(ui, dest=dest, **opts)
220
221
222 def reposetup(ui, repo):
223 if isinstance(repo.store, gitstore):
224 orig = repo.__class__
225 repo.store._progress_factory = repo.ui.makeprogress
226
227 class gitlocalrepo(orig):
228 def _makedirstate(self):
229 # TODO narrow support here
230 return dirstate.gitdirstate(
231 self.ui, self.vfs.base, self.store.git
232 )
233
234 def commit(self, *args, **kwargs):
235 ret = orig.commit(self, *args, **kwargs)
236 tid = self.store.git[gitutil.togitnode(ret)].tree.id
237 # DANGER! This will flush any writes staged to the
238 # index in Git, but we're sidestepping the index in a
239 # way that confuses git when we commit. Alas.
240 self.store.git.index.read_tree(tid)
241 self.store.git.index.write()
242 return ret
243
244 @property
245 def _bookmarks(self):
246 return gitbmstore(self.store.git)
247
248 repo.__class__ = gitlocalrepo
249 return repo
250
251
252 def extsetup(ui):
253 extensions.wrapfunction(localrepo, b'makestore', _makestore)
254 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
255 # Inject --git flag for `hg init`
256 entry = extensions.wrapcommand(commands.table, b'init', init)
257 entry[1].extend(
258 [(b'', b'git', None, b'setup up a git repository instead of hg')]
259 )
@@ -0,0 +1,295 b''
1 from __future__ import absolute_import
2
3 import contextlib
4 import errno
5 import os
6
7 import pygit2
8
9 from mercurial import (
10 error,
11 extensions,
12 match as matchmod,
13 node as nodemod,
14 pycompat,
15 scmutil,
16 util,
17 )
18 from mercurial.interfaces import (
19 dirstate as intdirstate,
20 util as interfaceutil,
21 )
22
23 from . import gitutil
24
25
26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
28 return orig(filepath, warn, sourceinfo=False)
29 result = []
30 warnings = []
31 with open(filepath, b'rb') as fp:
32 for l in fp:
33 l = l.strip()
34 if not l or l.startswith(b'#'):
35 continue
36 if l.startswith(b'!'):
37 warnings.append(b'unsupported ignore pattern %s' % l)
38 continue
39 if l.startswith(b'/'):
40 result.append(b'rootglob:' + l[1:])
41 else:
42 result.append(b'relglob:' + l)
43 return result, warnings
44
45
46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
47
48
49 _STATUS_MAP = {
50 pygit2.GIT_STATUS_CONFLICTED: b'm',
51 pygit2.GIT_STATUS_CURRENT: b'n',
52 pygit2.GIT_STATUS_IGNORED: b'?',
53 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
54 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
55 pygit2.GIT_STATUS_INDEX_NEW: b'a',
56 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
57 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
58 pygit2.GIT_STATUS_WT_DELETED: b'r',
59 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
60 pygit2.GIT_STATUS_WT_NEW: b'?',
61 pygit2.GIT_STATUS_WT_RENAMED: b'a',
62 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
63 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
64 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm',
65 }
66
67
68 @interfaceutil.implementer(intdirstate.idirstate)
69 class gitdirstate(object):
70 def __init__(self, ui, root, gitrepo):
71 self._ui = ui
72 self._root = os.path.dirname(root)
73 self.git = gitrepo
74 self._plchangecallbacks = {}
75
76 def p1(self):
77 return self.git.head.peel().id.raw
78
79 def p2(self):
80 # TODO: MERGE_HEAD? something like that, right?
81 return nodemod.nullid
82
83 def setparents(self, p1, p2=nodemod.nullid):
84 assert p2 == nodemod.nullid, b'TODO merging support'
85 self.git.head.set_target(gitutil.togitnode(p1))
86
87 @util.propertycache
88 def identity(self):
89 return util.filestat.frompath(
90 os.path.join(self._root, b'.git', b'index')
91 )
92
93 def branch(self):
94 return b'default'
95
96 def parents(self):
97 # TODO how on earth do we find p2 if a merge is in flight?
98 return self.p1(), nodemod.nullid
99
100 def __iter__(self):
101 return (pycompat.fsencode(f.path) for f in self.git.index)
102
103 def items(self):
104 for ie in self.git.index:
105 yield ie.path, None # value should be a dirstatetuple
106
107 # py2,3 compat forward
108 iteritems = items
109
110 def __getitem__(self, filename):
111 try:
112 gs = self.git.status_file(filename)
113 except KeyError:
114 return b'?'
115 return _STATUS_MAP[gs]
116
117 def __contains__(self, filename):
118 try:
119 gs = self.git.status_file(filename)
120 return _STATUS_MAP[gs] != b'?'
121 except KeyError:
122 return False
123
124 def status(self, match, subrepos, ignored, clean, unknown):
125 # TODO handling of clean files - can we get that from git.status()?
126 modified, added, removed, deleted, unknown, ignored, clean = (
127 [],
128 [],
129 [],
130 [],
131 [],
132 [],
133 [],
134 )
135 gstatus = self.git.status()
136 for path, status in gstatus.items():
137 path = pycompat.fsencode(path)
138 if status == pygit2.GIT_STATUS_IGNORED:
139 if path.endswith(b'/'):
140 continue
141 ignored.append(path)
142 elif status in (
143 pygit2.GIT_STATUS_WT_MODIFIED,
144 pygit2.GIT_STATUS_INDEX_MODIFIED,
145 pygit2.GIT_STATUS_WT_MODIFIED
146 | pygit2.GIT_STATUS_INDEX_MODIFIED,
147 ):
148 modified.append(path)
149 elif status == pygit2.GIT_STATUS_INDEX_NEW:
150 added.append(path)
151 elif status == pygit2.GIT_STATUS_WT_NEW:
152 unknown.append(path)
153 elif status == pygit2.GIT_STATUS_WT_DELETED:
154 deleted.append(path)
155 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
156 removed.append(path)
157 else:
158 raise error.Abort(
159 b'unhandled case: status for %r is %r' % (path, status)
160 )
161
162 # TODO are we really always sure of status here?
163 return (
164 False,
165 scmutil.status(
166 modified, added, removed, deleted, unknown, ignored, clean
167 ),
168 )
169
170 def flagfunc(self, buildfallback):
171 # TODO we can do better
172 return buildfallback()
173
174 def getcwd(self):
175 # TODO is this a good way to do this?
176 return os.path.dirname(
177 os.path.dirname(pycompat.fsencode(self.git.path))
178 )
179
180 def normalize(self, path):
181 normed = util.normcase(path)
182 assert normed == path, b"TODO handling of case folding: %s != %s" % (
183 normed,
184 path,
185 )
186 return path
187
188 @property
189 def _checklink(self):
190 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
191
192 def copies(self):
193 # TODO support copies?
194 return {}
195
196 # # TODO what the heck is this
197 _filecache = set()
198
199 def pendingparentchange(self):
200 # TODO: we need to implement the context manager bits and
201 # correctly stage/revert index edits.
202 return False
203
204 def write(self, tr):
205 # TODO: call parent change callbacks
206
207 if tr:
208
209 def writeinner(category):
210 self.git.index.write()
211
212 tr.addpending(b'gitdirstate', writeinner)
213 else:
214 self.git.index.write()
215
216 def pathto(self, f, cwd=None):
217 if cwd is None:
218 cwd = self.getcwd()
219 # TODO core dirstate does something about slashes here
220 assert isinstance(f, bytes)
221 r = util.pathto(self._root, cwd, f)
222 return r
223
224 def matches(self, match):
225 for x in self.git.index:
226 p = pycompat.fsencode(x.path)
227 if match(p):
228 yield p
229
230 def normal(self, f, parentfiledata=None):
231 """Mark a file normal and clean."""
232 # TODO: for now we just let libgit2 re-stat the file. We can
233 # clearly do better.
234
235 def normallookup(self, f):
236 """Mark a file normal, but possibly dirty."""
237 # TODO: for now we just let libgit2 re-stat the file. We can
238 # clearly do better.
239
240 def walk(self, match, subrepos, unknown, ignored, full=True):
241 # TODO: we need to use .status() and not iterate the index,
242 # because the index doesn't force a re-walk and so `hg add` of
243 # a new file without an intervening call to status will
244 # silently do nothing.
245 r = {}
246 cwd = self.getcwd()
247 for path, status in self.git.status().items():
248 if path.startswith('.hg/'):
249 continue
250 path = pycompat.fsencode(path)
251 if not match(path):
252 continue
253 # TODO construct the stat info from the status object?
254 try:
255 s = os.stat(os.path.join(cwd, path))
256 except OSError as e:
257 if e.errno != errno.ENOENT:
258 raise
259 continue
260 r[path] = s
261 return r
262
263 def savebackup(self, tr, backupname):
264 # TODO: figure out a strategy for saving index backups.
265 pass
266
267 def restorebackup(self, tr, backupname):
268 # TODO: figure out a strategy for saving index backups.
269 pass
270
271 def add(self, f):
272 self.git.index.add(pycompat.fsdecode(f))
273
274 def drop(self, f):
275 self.git.index.remove(pycompat.fsdecode(f))
276
277 def remove(self, f):
278 self.git.index.remove(pycompat.fsdecode(f))
279
280 def copied(self, path):
281 # TODO: track copies?
282 return None
283
284 @contextlib.contextmanager
285 def parentchange(self):
286 # TODO: track this maybe?
287 yield
288
289 def addparentchangecallback(self, category, callback):
290 # TODO: should this be added to the dirstate interface?
291 self._plchangecallbacks[category] = callback
292
293 def clearbackup(self, tr, backupname):
294 # TODO
295 pass
@@ -0,0 +1,463 b''
1 from __future__ import absolute_import
2
3 import pygit2
4
5 from mercurial.i18n import _
6
7 from mercurial import (
8 ancestor,
9 changelog as hgchangelog,
10 dagop,
11 encoding,
12 error,
13 manifest,
14 node as nodemod,
15 pycompat,
16 )
17 from mercurial.interfaces import (
18 repository,
19 util as interfaceutil,
20 )
21 from mercurial.utils import stringutil
22 from . import (
23 gitutil,
24 index,
25 manifest as gitmanifest,
26 )
27
28
29 class baselog(object): # revlog.revlog):
30 """Common implementations between changelog and manifestlog."""
31
32 def __init__(self, gr, db):
33 self.gitrepo = gr
34 self._db = db
35
36 def __len__(self):
37 return int(
38 self._db.execute('SELECT COUNT(*) FROM changelog').fetchone()[0]
39 )
40
41 def rev(self, n):
42 if n == nodemod.nullid:
43 return -1
44 t = self._db.execute(
45 'SELECT rev FROM changelog WHERE node = ?', (gitutil.togitnode(n),)
46 ).fetchone()
47 if t is None:
48 raise error.LookupError(n, b'00changelog.i', _(b'no node %d'))
49 return t[0]
50
51 def node(self, r):
52 if r == nodemod.nullrev:
53 return nodemod.nullid
54 t = self._db.execute(
55 'SELECT node FROM changelog WHERE rev = ?', (r,)
56 ).fetchone()
57 if t is None:
58 raise error.LookupError(r, b'00changelog.i', _(b'no node'))
59 return nodemod.bin(t[0])
60
61 def hasnode(self, n):
62 t = self._db.execute(
63 'SELECT node FROM changelog WHERE node = ?', (n,)
64 ).fetchone()
65 return t is not None
66
67
68 class baselogindex(object):
69 def __init__(self, log):
70 self._log = log
71
72 def has_node(self, n):
73 return self._log.rev(n) != -1
74
75 def __len__(self):
76 return len(self._log)
77
78 def __getitem__(self, idx):
79 p1rev, p2rev = self._log.parentrevs(idx)
80 # TODO: it's messy that the index leaks so far out of the
81 # storage layer that we have to implement things like reading
82 # this raw tuple, which exposes revlog internals.
83 return (
84 # Pretend offset is just the index, since we don't really care.
85 idx,
86 # Same with lengths
87 idx, # length
88 idx, # rawsize
89 -1, # delta base
90 idx, # linkrev TODO is this right?
91 p1rev,
92 p2rev,
93 self._log.node(idx),
94 )
95
96
97 # TODO: an interface for the changelog type?
98 class changelog(baselog):
99 def __contains__(self, rev):
100 try:
101 self.node(rev)
102 return True
103 except error.LookupError:
104 return False
105
106 @property
107 def filteredrevs(self):
108 # TODO: we should probably add a refs/hg/ namespace for hidden
109 # heads etc, but that's an idea for later.
110 return set()
111
112 @property
113 def index(self):
114 return baselogindex(self)
115
116 @property
117 def nodemap(self):
118 r = {
119 nodemod.bin(v[0]): v[1]
120 for v in self._db.execute('SELECT node, rev FROM changelog')
121 }
122 r[nodemod.nullid] = nodemod.nullrev
123 return r
124
125 def tip(self):
126 t = self._db.execute(
127 'SELECT node FROM changelog ORDER BY rev DESC LIMIT 1'
128 ).fetchone()
129 if t:
130 return nodemod.bin(t[0])
131 return nodemod.nullid
132
133 def revs(self, start=0, stop=None):
134 if stop is None:
135 stop = self.tip()
136 t = self._db.execute(
137 'SELECT rev FROM changelog '
138 'WHERE rev >= ? AND rev <= ? '
139 'ORDER BY REV ASC',
140 (start, stop),
141 )
142 return (int(r[0]) for r in t)
143
144 def _partialmatch(self, id):
145 if nodemod.wdirhex.startswith(id):
146 raise error.WdirUnsupported
147 candidates = [
148 nodemod.bin(x[0])
149 for x in self._db.execute(
150 'SELECT node FROM changelog WHERE node LIKE ?', (id + b'%',)
151 )
152 ]
153 if nodemod.nullhex.startswith(id):
154 candidates.append(nodemod.nullid)
155 if len(candidates) > 1:
156 raise error.AmbiguousPrefixLookupError(
157 id, b'00changelog.i', _(b'ambiguous identifier')
158 )
159 if candidates:
160 return candidates[0]
161 return None
162
163 def flags(self, rev):
164 return 0
165
166 def shortest(self, node, minlength=1):
167 nodehex = nodemod.hex(node)
168 for attempt in pycompat.xrange(minlength, len(nodehex) + 1):
169 candidate = nodehex[:attempt]
170 matches = int(
171 self._db.execute(
172 'SELECT COUNT(*) FROM changelog WHERE node LIKE ?',
173 (pycompat.sysstr(nodehex + b'%'),),
174 ).fetchone()[0]
175 )
176 if matches == 1:
177 return candidate
178 return nodehex
179
180 def headrevs(self, revs=None):
181 realheads = [
182 int(x[0])
183 for x in self._db.execute(
184 'SELECT rev FROM changelog '
185 'INNER JOIN heads ON changelog.node = heads.node'
186 )
187 ]
188 if revs:
189 return sorted([r for r in revs if r in realheads])
190 return sorted(realheads)
191
192 def changelogrevision(self, nodeorrev):
193 # Ensure we have a node id
194 if isinstance(nodeorrev, int):
195 n = self.node(nodeorrev)
196 else:
197 n = nodeorrev
198 # handle looking up nullid
199 if n == nodemod.nullid:
200 return hgchangelog._changelogrevision(extra={})
201 hn = gitutil.togitnode(n)
202 # We've got a real commit!
203 files = [
204 r[0]
205 for r in self._db.execute(
206 'SELECT filename FROM changedfiles '
207 'WHERE node = ? and filenode != ?',
208 (hn, gitutil.nullgit),
209 )
210 ]
211 filesremoved = [
212 r[0]
213 for r in self._db.execute(
214 'SELECT filename FROM changedfiles '
215 'WHERE node = ? and filenode = ?',
216 (hn, nodemod.nullhex),
217 )
218 ]
219 c = self.gitrepo[hn]
220 return hgchangelog._changelogrevision(
221 manifest=n, # pretend manifest the same as the commit node
222 user=b'%s <%s>'
223 % (c.author.name.encode('utf8'), c.author.email.encode('utf8')),
224 date=(c.author.time, -c.author.offset * 60),
225 files=files,
226 # TODO filesadded in the index
227 filesremoved=filesremoved,
228 description=c.message.encode('utf8'),
229 # TODO do we want to handle extra? how?
230 extra={b'branch': b'default'},
231 )
232
233 def ancestors(self, revs, stoprev=0, inclusive=False):
234 revs = list(revs)
235 tip = self.rev(self.tip())
236 for r in revs:
237 if r > tip:
238 raise IndexError(b'Invalid rev %r' % r)
239 return ancestor.lazyancestors(
240 self.parentrevs, revs, stoprev=stoprev, inclusive=inclusive
241 )
242
243 # Cleanup opportunity: this is *identical* to the revlog.py version
244 def descendants(self, revs):
245 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
246
247 def reachableroots(self, minroot, heads, roots, includepath=False):
248 return dagop._reachablerootspure(
249 self.parentrevs, minroot, roots, heads, includepath
250 )
251
252 # Cleanup opportunity: this is *identical* to the revlog.py version
253 def isancestor(self, a, b):
254 a, b = self.rev(a), self.rev(b)
255 return self.isancestorrev(a, b)
256
257 # Cleanup opportunity: this is *identical* to the revlog.py version
258 def isancestorrev(self, a, b):
259 if a == nodemod.nullrev:
260 return True
261 elif a == b:
262 return True
263 elif a > b:
264 return False
265 return bool(self.reachableroots(a, [b], [a], includepath=False))
266
267 def parentrevs(self, rev):
268 n = self.node(rev)
269 hn = gitutil.togitnode(n)
270 c = self.gitrepo[hn]
271 p1 = p2 = nodemod.nullrev
272 if c.parents:
273 p1 = self.rev(c.parents[0].id.raw)
274 if len(c.parents) > 2:
275 raise error.Abort(b'TODO octopus merge handling')
276 if len(c.parents) == 2:
277 p2 = self.rev(c.parents[0].id.raw)
278 return p1, p2
279
280 # Private method is used at least by the tags code.
281 _uncheckedparentrevs = parentrevs
282
283 def commonancestorsheads(self, a, b):
284 # TODO the revlog verson of this has a C path, so we probably
285 # need to optimize this...
286 a, b = self.rev(a), self.rev(b)
287 return [
288 self.node(n)
289 for n in ancestor.commonancestorsheads(self.parentrevs, a, b)
290 ]
291
292 def branchinfo(self, rev):
293 """Git doesn't do named branches, so just put everything on default."""
294 return b'default', False
295
296 def delayupdate(self, tr):
297 # TODO: I think we can elide this because we're just dropping
298 # an object in the git repo?
299 pass
300
301 def add(
302 self,
303 manifest,
304 files,
305 desc,
306 transaction,
307 p1,
308 p2,
309 user,
310 date=None,
311 extra=None,
312 p1copies=None,
313 p2copies=None,
314 filesadded=None,
315 filesremoved=None,
316 ):
317 parents = []
318 hp1, hp2 = gitutil.togitnode(p1), gitutil.togitnode(p2)
319 if p1 != nodemod.nullid:
320 parents.append(hp1)
321 if p2 and p2 != nodemod.nullid:
322 parents.append(hp2)
323 assert date is not None
324 timestamp, tz = date
325 sig = pygit2.Signature(
326 encoding.unifromlocal(stringutil.person(user)),
327 encoding.unifromlocal(stringutil.email(user)),
328 timestamp,
329 -(tz // 60),
330 )
331 oid = self.gitrepo.create_commit(
332 None, sig, sig, desc, gitutil.togitnode(manifest), parents
333 )
334 # Set up an internal reference to force the commit into the
335 # changelog. Hypothetically, we could even use this refs/hg/
336 # namespace to allow for anonymous heads on git repos, which
337 # would be neat.
338 self.gitrepo.references.create(
339 'refs/hg/internal/latest-commit', oid, force=True
340 )
341 # Reindex now to pick up changes. We omit the progress
342 # callback because this will be very quick.
343 index._index_repo(self.gitrepo, self._db)
344 return oid.raw
345
346
347 class manifestlog(baselog):
348 def __getitem__(self, node):
349 return self.get(b'', node)
350
351 def get(self, relpath, node):
352 if node == nodemod.nullid:
353 # TODO: this should almost certainly be a memgittreemanifestctx
354 return manifest.memtreemanifestctx(self, relpath)
355 commit = self.gitrepo[gitutil.togitnode(node)]
356 t = commit.tree
357 if relpath:
358 parts = relpath.split(b'/')
359 for p in parts:
360 te = t[p]
361 t = self.gitrepo[te.id]
362 return gitmanifest.gittreemanifestctx(self.gitrepo, t)
363
364
365 @interfaceutil.implementer(repository.ifilestorage)
366 class filelog(baselog):
367 def __init__(self, gr, db, path):
368 super(filelog, self).__init__(gr, db)
369 assert isinstance(path, bytes)
370 self.path = path
371
372 def read(self, node):
373 if node == nodemod.nullid:
374 return b''
375 return self.gitrepo[gitutil.togitnode(node)].data
376
377 def lookup(self, node):
378 if len(node) not in (20, 40):
379 node = int(node)
380 if isinstance(node, int):
381 assert False, b'todo revnums for nodes'
382 if len(node) == 40:
383 node = nodemod.bin(node)
384 hnode = gitutil.togitnode(node)
385 if hnode in self.gitrepo:
386 return node
387 raise error.LookupError(self.path, node, _(b'no match found'))
388
389 def cmp(self, node, text):
390 """Returns True if text is different than content at `node`."""
391 return self.read(node) != text
392
393 def add(self, text, meta, transaction, link, p1=None, p2=None):
394 assert not meta # Should we even try to handle this?
395 return self.gitrepo.create_blob(text).raw
396
397 def __iter__(self):
398 for clrev in self._db.execute(
399 '''
400 SELECT rev FROM changelog
401 INNER JOIN changedfiles ON changelog.node = changedfiles.node
402 WHERE changedfiles.filename = ? AND changedfiles.filenode != ?
403 ''',
404 (pycompat.fsdecode(self.path), gitutil.nullgit),
405 ):
406 yield clrev[0]
407
408 def linkrev(self, fr):
409 return fr
410
411 def rev(self, node):
412 row = self._db.execute(
413 '''
414 SELECT rev FROM changelog
415 INNER JOIN changedfiles ON changelog.node = changedfiles.node
416 WHERE changedfiles.filename = ? AND changedfiles.filenode = ?''',
417 (pycompat.fsdecode(self.path), gitutil.togitnode(node)),
418 ).fetchone()
419 if row is None:
420 raise error.LookupError(self.path, node, _(b'no such node'))
421 return int(row[0])
422
423 def node(self, rev):
424 maybe = self._db.execute(
425 '''SELECT filenode FROM changedfiles
426 INNER JOIN changelog ON changelog.node = changedfiles.node
427 WHERE changelog.rev = ? AND filename = ?
428 ''',
429 (rev, pycompat.fsdecode(self.path)),
430 ).fetchone()
431 if maybe is None:
432 raise IndexError('gitlog %r out of range %d' % (self.path, rev))
433 return nodemod.bin(maybe[0])
434
435 def parents(self, node):
436 gn = gitutil.togitnode(node)
437 gp = pycompat.fsdecode(self.path)
438 ps = []
439 for p in self._db.execute(
440 '''SELECT p1filenode, p2filenode FROM changedfiles
441 WHERE filenode = ? AND filename = ?
442 ''',
443 (gn, gp),
444 ).fetchone():
445 if p is None:
446 commit = self._db.execute(
447 "SELECT node FROM changedfiles "
448 "WHERE filenode = ? AND filename = ?",
449 (gn, gp),
450 ).fetchone()[0]
451 # This filelog is missing some data. Build the
452 # filelog, then recurse (which will always find data).
453 if pycompat.ispy3:
454 commit = commit.decode('ascii')
455 index.fill_in_filelog(self.gitrepo, self._db, commit, gp, gn)
456 return self.parents(node)
457 else:
458 ps.append(nodemod.bin(p))
459 return ps
460
461 def renamed(self, node):
462 # TODO: renames/copies
463 return False
@@ -0,0 +1,26 b''
1 """utilities to assist in working with pygit2"""
2 from __future__ import absolute_import
3
4 from mercurial.node import bin, hex, nullid
5
6 from mercurial import pycompat
7
8
9 def togitnode(n):
10 """Wrapper to convert a Mercurial binary node to a unicode hexlified node.
11
12 pygit2 and sqlite both need nodes as strings, not bytes.
13 """
14 assert len(n) == 20
15 return pycompat.sysstr(hex(n))
16
17
18 def fromgitnode(n):
19 """Opposite of togitnode."""
20 assert len(n) == 40
21 if pycompat.ispy3:
22 return bin(n.encode('ascii'))
23 return bin(n)
24
25
26 nullgit = togitnode(nullid)
@@ -0,0 +1,346 b''
1 from __future__ import absolute_import
2
3 import collections
4 import os
5 import sqlite3
6
7 import pygit2
8
9 from mercurial.i18n import _
10
11 from mercurial import (
12 encoding,
13 error,
14 node as nodemod,
15 pycompat,
16 )
17
18 from . import gitutil
19
20
21 _CURRENT_SCHEMA_VERSION = 1
22 _SCHEMA = (
23 """
24 CREATE TABLE refs (
25 -- node and name are unique together. There may be more than one name for
26 -- a given node, and there may be no name at all for a given node (in the
27 -- case of an anonymous hg head).
28 node TEXT NOT NULL,
29 name TEXT
30 );
31
32 -- The "possible heads" of the repository, which we use to figure out
33 -- if we need to re-walk the changelog.
34 CREATE TABLE possible_heads (
35 node TEXT NOT NULL
36 );
37
38 -- The topological heads of the changelog, which hg depends on.
39 CREATE TABLE heads (
40 node TEXT NOT NULL
41 );
42
43 -- A total ordering of the changelog
44 CREATE TABLE changelog (
45 rev INTEGER NOT NULL PRIMARY KEY,
46 node TEXT NOT NULL,
47 p1 TEXT,
48 p2 TEXT
49 );
50
51 CREATE UNIQUE INDEX changelog_node_idx ON changelog(node);
52 CREATE UNIQUE INDEX changelog_node_rev_idx ON changelog(rev, node);
53
54 -- Changed files for each commit, which lets us dynamically build
55 -- filelogs.
56 CREATE TABLE changedfiles (
57 node TEXT NOT NULL,
58 filename TEXT NOT NULL,
59 -- 40 zeroes for deletions
60 filenode TEXT NOT NULL,
61 -- to handle filelog parentage:
62 p1node TEXT,
63 p1filenode TEXT,
64 p2node TEXT,
65 p2filenode TEXT
66 );
67
68 CREATE INDEX changedfiles_nodes_idx
69 ON changedfiles(node);
70
71 PRAGMA user_version=%d
72 """
73 % _CURRENT_SCHEMA_VERSION
74 )
75
76
77 def _createdb(path):
78 # print('open db', path)
79 # import traceback
80 # traceback.print_stack()
81 db = sqlite3.connect(encoding.strfromlocal(path))
82 db.text_factory = bytes
83
84 res = db.execute('PRAGMA user_version').fetchone()[0]
85
86 # New database.
87 if res == 0:
88 for statement in _SCHEMA.split(';'):
89 db.execute(statement.strip())
90
91 db.commit()
92
93 elif res == _CURRENT_SCHEMA_VERSION:
94 pass
95
96 else:
97 raise error.Abort(_(b'sqlite database has unrecognized version'))
98
99 db.execute('PRAGMA journal_mode=WAL')
100
101 return db
102
103
104 _OUR_ORDER = (
105 pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_REVERSE
106 )
107
108 _DIFF_FLAGS = 1 << 21 # GIT_DIFF_FORCE_BINARY, which isn't exposed by pygit2
109
110
111 def _find_nearest_ancestor_introducing_node(
112 db, gitrepo, file_path, walk_start, filenode
113 ):
114 """Find the nearest ancestor that introduces a file node.
115
116 Args:
117 db: a handle to our sqlite database.
118 gitrepo: A pygit2.Repository instance.
119 file_path: the path of a file in the repo
120 walk_start: a pygit2.Oid that is a commit where we should start walking
121 for our nearest ancestor.
122
123 Returns:
124 A hexlified SHA that is the commit ID of the next-nearest parent.
125 """
126 assert isinstance(file_path, str), 'file_path must be str, got %r' % type(
127 file_path
128 )
129 assert isinstance(filenode, str), 'filenode must be str, got %r' % type(
130 filenode
131 )
132 parent_options = {
133 row[0].decode('ascii')
134 for row in db.execute(
135 'SELECT node FROM changedfiles '
136 'WHERE filename = ? AND filenode = ?',
137 (file_path, filenode),
138 )
139 }
140 inner_walker = gitrepo.walk(walk_start, _OUR_ORDER)
141 for w in inner_walker:
142 if w.id.hex in parent_options:
143 return w.id.hex
144 raise error.ProgrammingError(
145 'Unable to find introducing commit for %s node %s from %s',
146 (file_path, filenode, walk_start),
147 )
148
149
150 def fill_in_filelog(gitrepo, db, startcommit, path, startfilenode):
151 """Given a starting commit and path, fill in a filelog's parent pointers.
152
153 Args:
154 gitrepo: a pygit2.Repository
155 db: a handle to our sqlite database
156 startcommit: a hexlified node id for the commit to start at
157 path: the path of the file whose parent pointers we should fill in.
158 filenode: the hexlified node id of the file at startcommit
159
160 TODO: make filenode optional
161 """
162 assert isinstance(
163 startcommit, str
164 ), 'startcommit must be str, got %r' % type(startcommit)
165 assert isinstance(
166 startfilenode, str
167 ), 'startfilenode must be str, got %r' % type(startfilenode)
168 visit = collections.deque([(startcommit, startfilenode)])
169 while visit:
170 cnode, filenode = visit.popleft()
171 commit = gitrepo[cnode]
172 parents = []
173 for parent in commit.parents:
174 t = parent.tree
175 for comp in path.split('/'):
176 try:
177 t = gitrepo[t[comp].id]
178 except KeyError:
179 break
180 else:
181 introducer = _find_nearest_ancestor_introducing_node(
182 db, gitrepo, path, parent.id, t.id.hex
183 )
184 parents.append((introducer, t.id.hex))
185 p1node = p1fnode = p2node = p2fnode = gitutil.nullgit
186 for par, parfnode in parents:
187 found = int(
188 db.execute(
189 'SELECT COUNT(*) FROM changedfiles WHERE '
190 'node = ? AND filename = ? AND filenode = ? AND '
191 'p1node NOT NULL',
192 (par, path, parfnode),
193 ).fetchone()[0]
194 )
195 if found == 0:
196 assert par is not None
197 visit.append((par, parfnode))
198 if parents:
199 p1node, p1fnode = parents[0]
200 if len(parents) == 2:
201 p2node, p2fnode = parents[1]
202 if len(parents) > 2:
203 raise error.ProgrammingError(
204 b"git support can't handle octopus merges"
205 )
206 db.execute(
207 'UPDATE changedfiles SET '
208 'p1node = ?, p1filenode = ?, p2node = ?, p2filenode = ? '
209 'WHERE node = ? AND filename = ? AND filenode = ?',
210 (p1node, p1fnode, p2node, p2fnode, commit.id.hex, path, filenode),
211 )
212 db.commit()
213
214
215 def _index_repo(gitrepo, db, progress_factory=lambda *args, **kwargs: None):
216 # Identify all references so we can tell the walker to visit all of them.
217 all_refs = gitrepo.listall_references()
218 possible_heads = set()
219 prog = progress_factory(b'refs')
220 for pos, ref in enumerate(all_refs):
221 if prog is not None:
222 prog.update(pos)
223 if not (
224 ref.startswith('refs/heads/') # local branch
225 or ref.startswith('refs/tags/') # tag
226 or ref.startswith('refs/remotes/') # remote branch
227 or ref.startswith('refs/hg/') # from this extension
228 ):
229 continue
230 try:
231 start = gitrepo.lookup_reference(ref).peel(pygit2.GIT_OBJ_COMMIT)
232 except ValueError:
233 # No commit to be found, so we don't care for hg's purposes.
234 continue
235 possible_heads.add(start.id)
236 # Optimization: if the list of heads hasn't changed, don't
237 # reindex, the changelog. This doesn't matter on small
238 # repositories, but on even moderately deep histories (eg cpython)
239 # this is a very important performance win.
240 #
241 # TODO: we should figure out how to incrementally index history
242 # (preferably by detecting rewinds!) so that we don't have to do a
243 # full changelog walk every time a new commit is created.
244 cache_heads = {x[0] for x in db.execute('SELECT node FROM possible_heads')}
245 walker = None
246 cur_cache_heads = {h.hex for h in possible_heads}
247 if cur_cache_heads == cache_heads:
248 return
249 for start in possible_heads:
250 if walker is None:
251 walker = gitrepo.walk(start, _OUR_ORDER)
252 else:
253 walker.push(start)
254
255 # Empty out the existing changelog. Even for large-ish histories
256 # we can do the top-level "walk all the commits" dance very
257 # quickly as long as we don't need to figure out the changed files
258 # list.
259 db.execute('DELETE FROM changelog')
260 if prog is not None:
261 prog.complete()
262 prog = progress_factory(b'commits')
263 # This walker is sure to visit all the revisions in history, but
264 # only once.
265 for pos, commit in enumerate(walker):
266 if prog is not None:
267 prog.update(pos)
268 p1 = p2 = nodemod.nullhex
269 if len(commit.parents) > 2:
270 raise error.ProgrammingError(
271 (
272 b"git support can't handle octopus merges, "
273 b"found a commit with %d parents :("
274 )
275 % len(commit.parents)
276 )
277 if commit.parents:
278 p1 = commit.parents[0].id.hex
279 if len(commit.parents) == 2:
280 p2 = commit.parents[1].id.hex
281 db.execute(
282 'INSERT INTO changelog (rev, node, p1, p2) VALUES(?, ?, ?, ?)',
283 (pos, commit.id.hex, p1, p2),
284 )
285
286 num_changedfiles = db.execute(
287 "SELECT COUNT(*) from changedfiles WHERE node = ?",
288 (commit.id.hex,),
289 ).fetchone()[0]
290 if not num_changedfiles:
291 files = {}
292 # I *think* we only need to check p1 for changed files
293 # (and therefore linkrevs), because any node that would
294 # actually have this commit as a linkrev would be
295 # completely new in this rev.
296 p1 = commit.parents[0].id.hex if commit.parents else None
297 if p1 is not None:
298 patchgen = gitrepo.diff(p1, commit.id.hex, flags=_DIFF_FLAGS)
299 else:
300 patchgen = commit.tree.diff_to_tree(
301 swap=True, flags=_DIFF_FLAGS
302 )
303 new_files = (p.delta.new_file for p in patchgen)
304 files = {
305 nf.path: nf.id.hex
306 for nf in new_files
307 if nf.id.raw != nodemod.nullid
308 }
309 for p, n in files.items():
310 # We intentionally set NULLs for any file parentage
311 # information so it'll get demand-computed later. We
312 # used to do it right here, and it was _very_ slow.
313 db.execute(
314 'INSERT INTO changedfiles ('
315 'node, filename, filenode, p1node, p1filenode, p2node, '
316 'p2filenode) VALUES(?, ?, ?, ?, ?, ?, ?)',
317 (commit.id.hex, p, n, None, None, None, None),
318 )
319 db.execute('DELETE FROM heads')
320 db.execute('DELETE FROM possible_heads')
321 for hid in possible_heads:
322 h = hid.hex
323 db.execute('INSERT INTO possible_heads (node) VALUES(?)', (h,))
324 haschild = db.execute(
325 'SELECT COUNT(*) FROM changelog WHERE p1 = ? OR p2 = ?', (h, h)
326 ).fetchone()[0]
327 if not haschild:
328 db.execute('INSERT INTO heads (node) VALUES(?)', (h,))
329
330 db.commit()
331 if prog is not None:
332 prog.complete()
333
334
335 def get_index(gitrepo, progress_factory=lambda *args, **kwargs: None):
336 cachepath = os.path.join(
337 pycompat.fsencode(gitrepo.path), b'..', b'.hg', b'cache'
338 )
339 if not os.path.exists(cachepath):
340 os.makedirs(cachepath)
341 dbpath = os.path.join(cachepath, b'git-commits.sqlite')
342 db = _createdb(dbpath)
343 # TODO check against gitrepo heads before doing a full index
344 # TODO thread a ui.progress call into this layer
345 _index_repo(gitrepo, db, progress_factory)
346 return db
@@ -0,0 +1,293 b''
1 from __future__ import absolute_import
2
3 import pygit2
4
5 from mercurial import (
6 match as matchmod,
7 pathutil,
8 pycompat,
9 util,
10 )
11 from mercurial.interfaces import (
12 repository,
13 util as interfaceutil,
14 )
15 from . import gitutil
16
17
18 @interfaceutil.implementer(repository.imanifestdict)
19 class gittreemanifest(object):
20 """Expose git trees (and optionally a builder's overlay) as a manifestdict.
21
22 Very similar to mercurial.manifest.treemanifest.
23 """
24
25 def __init__(self, git_repo, root_tree, pending_changes):
26 """Initializer.
27
28 Args:
29 git_repo: The git_repo we're walking (required to look up child
30 trees).
31 root_tree: The root Git tree object for this manifest.
32 pending_changes: A dict in which pending changes will be
33 tracked. The enclosing memgittreemanifestctx will use this to
34 construct any required Tree objects in Git during it's
35 `write()` method.
36 """
37 self._git_repo = git_repo
38 self._tree = root_tree
39 if pending_changes is None:
40 pending_changes = {}
41 # dict of path: Optional[Tuple(node, flags)]
42 self._pending_changes = pending_changes
43
44 def _resolve_entry(self, path):
45 """Given a path, load its node and flags, or raise KeyError if missing.
46
47 This takes into account any pending writes in the builder.
48 """
49 upath = pycompat.fsdecode(path)
50 ent = None
51 if path in self._pending_changes:
52 val = self._pending_changes[path]
53 if val is None:
54 raise KeyError
55 return val
56 t = self._tree
57 comps = upath.split('/')
58 for comp in comps[:-1]:
59 te = self._tree[comp]
60 t = self._git_repo[te.id]
61 ent = t[comps[-1]]
62 if ent.filemode == pygit2.GIT_FILEMODE_BLOB:
63 flags = b''
64 elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE:
65 flags = b'x'
66 elif ent.filemode == pygit2.GIT_FILEMODE_LINK:
67 flags = b'l'
68 else:
69 raise ValueError('unsupported mode %s' % oct(ent.filemode))
70 return ent.id.raw, flags
71
72 def __getitem__(self, path):
73 return self._resolve_entry(path)[0]
74
75 def find(self, path):
76 return self._resolve_entry(path)
77
78 def __len__(self):
79 return len(list(self.walk(matchmod.always())))
80
81 def __nonzero__(self):
82 try:
83 next(iter(self))
84 return True
85 except StopIteration:
86 return False
87
88 __bool__ = __nonzero__
89
90 def __contains__(self, path):
91 try:
92 self._resolve_entry(path)
93 return True
94 except KeyError:
95 return False
96
97 def iterkeys(self):
98 return self.walk(matchmod.always())
99
100 def keys(self):
101 return list(self.iterkeys())
102
103 def __iter__(self):
104 return self.iterkeys()
105
106 def __setitem__(self, path, node):
107 self._pending_changes[path] = node, self.flags(path)
108
109 def __delitem__(self, path):
110 # TODO: should probably KeyError for already-deleted files?
111 self._pending_changes[path] = None
112
113 def filesnotin(self, other, match=None):
114 if match is not None:
115 match = matchmod.badmatch(match, lambda path, msg: None)
116 sm2 = set(other.walk(match))
117 return {f for f in self.walk(match) if f not in sm2}
118 return {f for f in self if f not in other}
119
120 @util.propertycache
121 def _dirs(self):
122 return pathutil.dirs(self)
123
124 def hasdir(self, dir):
125 return dir in self._dirs
126
127 def diff(self, other, match=None, clean=False):
128 # TODO
129 assert False
130
131 def setflag(self, path, flag):
132 node, unused_flag = self._resolve_entry(path)
133 self._pending_changes[path] = node, flag
134
135 def get(self, path, default=None):
136 try:
137 return self._resolve_entry(path)[0]
138 except KeyError:
139 return default
140
141 def flags(self, path):
142 try:
143 return self._resolve_entry(path)[1]
144 except KeyError:
145 return b''
146
147 def copy(self):
148 pass
149
150 def items(self):
151 for f in self:
152 # TODO: build a proper iterator version of this
153 yield self[f]
154
155 def iteritems(self):
156 return self.items()
157
158 def iterentries(self):
159 for f in self:
160 # TODO: build a proper iterator version of this
161 yield self._resolve_entry(f)
162
163 def text(self):
164 assert False # TODO can this method move out of the manifest iface?
165
166 def _walkonetree(self, tree, match, subdir):
167 for te in tree:
168 # TODO: can we prune dir walks with the matcher?
169 realname = subdir + pycompat.fsencode(te.name)
170 if te.type == r'tree':
171 for inner in self._walkonetree(
172 self._git_repo[te.id], match, realname + b'/'
173 ):
174 yield inner
175 if not match(realname):
176 continue
177 yield pycompat.fsencode(realname)
178
179 def walk(self, match):
180 # TODO: this is a very lazy way to merge in the pending
181 # changes. There is absolutely room for optimization here by
182 # being clever about walking over the sets...
183 baseline = set(self._walkonetree(self._tree, match, b''))
184 deleted = {p for p, v in self._pending_changes.items() if v is None}
185 pend = {p for p in self._pending_changes if match(p)}
186 return iter(sorted((baseline | pend) - deleted))
187
188
189 @interfaceutil.implementer(repository.imanifestrevisionstored)
190 class gittreemanifestctx(object):
191 def __init__(self, repo, gittree):
192 self._repo = repo
193 self._tree = gittree
194
195 def read(self):
196 return gittreemanifest(self._repo, self._tree, None)
197
198 def copy(self):
199 # NB: it's important that we return a memgittreemanifestctx
200 # because the caller expects a mutable manifest.
201 return memgittreemanifestctx(self._repo, self._tree)
202
203 def find(self, path):
204 self.read()[path]
205
206
207 @interfaceutil.implementer(repository.imanifestrevisionwritable)
208 class memgittreemanifestctx(object):
209 def __init__(self, repo, tree):
210 self._repo = repo
211 self._tree = tree
212 # dict of path: Optional[Tuple(node, flags)]
213 self._pending_changes = {}
214
215 def read(self):
216 return gittreemanifest(self._repo, self._tree, self._pending_changes)
217
218 def copy(self):
219 # TODO: if we have a builder in play, what should happen here?
220 # Maybe we can shuffle copy() into the immutable interface.
221 return memgittreemanifestctx(self._repo, self._tree)
222
223 def write(self, transaction, link, p1, p2, added, removed, match=None):
224 # We're not (for now, anyway) going to audit filenames, so we
225 # can ignore added and removed.
226
227 # TODO what does this match argument get used for? hopefully
228 # just narrow?
229 assert not match or isinstance(match, matchmod.alwaysmatcher)
230
231 touched_dirs = pathutil.dirs(self._pending_changes)
232 trees = {
233 b'': self._tree,
234 }
235 # path: treebuilder
236 builders = {
237 b'': self._repo.TreeBuilder(self._tree),
238 }
239 # get a TreeBuilder for every tree in the touched_dirs set
240 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
241 if d == b'':
242 # loaded root tree above
243 continue
244 comps = d.split(b'/')
245 full = b''
246 for part in comps:
247 parent = trees[full]
248 try:
249 new = self._repo[parent[pycompat.fsdecode(part)]]
250 except KeyError:
251 # new directory
252 new = None
253 full += b'/' + part
254 if new is not None:
255 # existing directory
256 trees[full] = new
257 builders[full] = self._repo.TreeBuilder(new)
258 else:
259 # new directory, use an empty dict to easily
260 # generate KeyError as any nested new dirs get
261 # created.
262 trees[full] = {}
263 builders[full] = self._repo.TreeBuilder()
264 for f, info in self._pending_changes.items():
265 if b'/' not in f:
266 dirname = b''
267 basename = f
268 else:
269 dirname, basename = f.rsplit(b'/', 1)
270 dirname = b'/' + dirname
271 if info is None:
272 builders[dirname].remove(pycompat.fsdecode(basename))
273 else:
274 n, fl = info
275 mode = {
276 b'': pygit2.GIT_FILEMODE_BLOB,
277 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
278 b'l': pygit2.GIT_FILEMODE_LINK,
279 }[fl]
280 builders[dirname].insert(
281 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
282 )
283 # This visits the buffered TreeBuilders in deepest-first
284 # order, bubbling up the edits.
285 for b in sorted(builders, key=len, reverse=True):
286 if b == b'':
287 break
288 cb = builders[b]
289 dn, bn = b.rsplit(b'/', 1)
290 builders[dn].insert(
291 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
292 )
293 return builders[b''].write().raw
@@ -0,0 +1,223 b''
1 This test requires pygit2:
2 > $PYTHON -c 'import pygit2' || exit 80
3
4 Setup:
5 > GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 > GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 > GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE
8 > GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 > GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11
12 > count=10
13 > gitcommit() {
14 > GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000";
15 > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
16 > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error"
17 > count=`expr $count + 1`
18 > }
19
20 > echo "[extensions]" >> $HGRCPATH
21 > echo "git=" >> $HGRCPATH
22
23 Make a new repo with git:
24 $ mkdir foo
25 $ cd foo
26 $ git init
27 Initialized empty Git repository in $TESTTMP/foo/.git/
28 Ignore the .hg directory within git:
29 $ echo .hg >> .git/info/exclude
30 $ echo alpha > alpha
31 $ git add alpha
32 $ gitcommit -am 'Add alpha'
33 $ echo beta > beta
34 $ git add beta
35 $ gitcommit -am 'Add beta'
36 $ echo gamma > gamma
37 $ git status
38 On branch master
39 Untracked files:
40 (use "git add <file>..." to include in what will be committed)
41 gamma
42
43 nothing added to commit but untracked files present (use "git add" to track)
44
45 Without creating the .hg, hg status fails:
46 $ hg status
47 abort: no repository found in '$TESTTMP/foo' (.hg not found)!
48 [255]
49 But if you run hg init --git, it works:
50 $ hg init --git
51 $ hg id --traceback
52 3d9be8deba43 tip master
53 $ hg status
54 ? gamma
55 Log works too:
56 $ hg log
57 changeset: 1:3d9be8deba43
58 bookmark: master
59 tag: tip
60 user: test <test@example.org>
61 date: Mon Jan 01 00:00:11 2007 +0000
62 summary: Add beta
63
64 changeset: 0:c5864c9d16fb
65 user: test <test@example.org>
66 date: Mon Jan 01 00:00:10 2007 +0000
67 summary: Add alpha
68
69
70
71 and bookmarks:
72 $ hg bookmarks
73 * master 1:3d9be8deba43
74
75 diff even works transparently in both systems:
76 $ echo blah >> alpha
77 $ git diff
78 diff --git a/alpha b/alpha
79 index 4a58007..faed1b7 100644
80 --- a/alpha
81 +++ b/alpha
82 @@ -1* +1,2 @@ (glob)
83 alpha
84 +blah
85 $ hg diff --git
86 diff --git a/alpha b/alpha
87 --- a/alpha
88 +++ b/alpha
89 @@ -1,1 +1,2 @@
90 alpha
91 +blah
92
93 Remove a file, it shows as such:
94 $ rm alpha
95 $ hg status
96 ! alpha
97 ? gamma
98
99 Revert works:
100 $ hg revert alpha --traceback
101 $ hg status
102 ? gamma
103 $ git status
104 On branch master
105 Untracked files:
106 (use "git add <file>..." to include in what will be committed)
107 gamma
108
109 nothing added to commit but untracked files present (use "git add" to track)
110
111 Add shows sanely in both:
112 $ hg add gamma
113 $ hg status
114 A gamma
115 $ hg files
116 alpha
117 beta
118 gamma
119 $ git ls-files
120 alpha
121 beta
122 gamma
123 $ git status
124 On branch master
125 Changes to be committed:
126 (use "git restore --staged <file>..." to unstage)
127 new file: gamma
128
129
130 forget does what it should as well:
131 $ hg forget gamma
132 $ hg status
133 ? gamma
134 $ git status
135 On branch master
136 Untracked files:
137 (use "git add <file>..." to include in what will be committed)
138 gamma
139
140 nothing added to commit but untracked files present (use "git add" to track)
141
142 clean up untracked file
143 $ rm gamma
144
145 hg log FILE
146
147 $ echo a >> alpha
148 $ hg ci -m 'more alpha' --traceback --date '1583522787 18000'
149 $ echo b >> beta
150 $ hg ci -m 'more beta'
151 $ echo a >> alpha
152 $ hg ci -m 'even more alpha'
153 $ hg log -G alpha
154 @ changeset: 4:6626247b7dc8
155 : bookmark: master
156 : tag: tip
157 : user: test <test>
158 : date: Thu Jan 01 00:00:00 1970 +0000
159 : summary: even more alpha
160 :
161 o changeset: 2:a1983dd7fb19
162 : user: test <test>
163 : date: Fri Mar 06 14:26:27 2020 -0500
164 : summary: more alpha
165 :
166 o changeset: 0:c5864c9d16fb
167 user: test <test@example.org>
168 date: Mon Jan 01 00:00:10 2007 +0000
169 summary: Add alpha
170
171 $ hg log -G beta
172 o changeset: 3:d8ee22687733
173 : user: test <test>
174 : date: Thu Jan 01 00:00:00 1970 +0000
175 : summary: more beta
176 :
177 o changeset: 1:3d9be8deba43
178 | user: test <test@example.org>
179 ~ date: Mon Jan 01 00:00:11 2007 +0000
180 summary: Add beta
181
182
183 node|shortest works correctly
184 $ hg log -r tip --template "{node|shortest}\n"
185 6626
186
187 hg annotate
188
189 $ hg annotate alpha
190 0: alpha
191 2: a
192 4: a
193 $ hg annotate beta
194 1: beta
195 3: b
196
197
198 Files in subdirectories. TODO: case-folding support, make this `A`
199 instead of `a`.
200
201 $ mkdir a
202 $ echo "This is file mu." > a/mu
203 $ hg ci -A -m 'Introduce file a/mu'
204 adding a/mu
205
206 Both hg and git agree a/mu is part of the repo
207
208 $ git ls-files
209 a/mu
210 alpha
211 beta
212 $ hg files
213 a/mu
214 alpha
215 beta
216
217 hg and git status both clean
218
219 $ git status
220 On branch master
221 nothing to commit, working tree clean
222 $ hg status
223
@@ -1,1712 +1,1713 b''
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6 from __future__ import print_function
6 from __future__ import print_function
7
7
8 import os
8 import os
9
9
10 # Mercurial will never work on Python 3 before 3.5 due to a lack
10 # Mercurial will never work on Python 3 before 3.5 due to a lack
11 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
11 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
12 # due to a bug in % formatting in bytestrings.
12 # due to a bug in % formatting in bytestrings.
13 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
13 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
14 # codecs.escape_encode() where it raises SystemError on empty bytestring
14 # codecs.escape_encode() where it raises SystemError on empty bytestring
15 # bug link: https://bugs.python.org/issue25270
15 # bug link: https://bugs.python.org/issue25270
16 supportedpy = ','.join(
16 supportedpy = ','.join(
17 [
17 [
18 '>=2.7',
18 '>=2.7',
19 '!=3.0.*',
19 '!=3.0.*',
20 '!=3.1.*',
20 '!=3.1.*',
21 '!=3.2.*',
21 '!=3.2.*',
22 '!=3.3.*',
22 '!=3.3.*',
23 '!=3.4.*',
23 '!=3.4.*',
24 '!=3.5.0',
24 '!=3.5.0',
25 '!=3.5.1',
25 '!=3.5.1',
26 '!=3.5.2',
26 '!=3.5.2',
27 '!=3.6.0',
27 '!=3.6.0',
28 '!=3.6.1',
28 '!=3.6.1',
29 ]
29 ]
30 )
30 )
31
31
32 import sys, platform
32 import sys, platform
33 import sysconfig
33 import sysconfig
34
34
35 if sys.version_info[0] >= 3:
35 if sys.version_info[0] >= 3:
36 printf = eval('print')
36 printf = eval('print')
37 libdir_escape = 'unicode_escape'
37 libdir_escape = 'unicode_escape'
38
38
39 def sysstr(s):
39 def sysstr(s):
40 return s.decode('latin-1')
40 return s.decode('latin-1')
41
41
42
42
43 else:
43 else:
44 libdir_escape = 'string_escape'
44 libdir_escape = 'string_escape'
45
45
46 def printf(*args, **kwargs):
46 def printf(*args, **kwargs):
47 f = kwargs.get('file', sys.stdout)
47 f = kwargs.get('file', sys.stdout)
48 end = kwargs.get('end', '\n')
48 end = kwargs.get('end', '\n')
49 f.write(b' '.join(args) + end)
49 f.write(b' '.join(args) + end)
50
50
51 def sysstr(s):
51 def sysstr(s):
52 return s
52 return s
53
53
54
54
55 # Attempt to guide users to a modern pip - this means that 2.6 users
55 # Attempt to guide users to a modern pip - this means that 2.6 users
56 # should have a chance of getting a 4.2 release, and when we ratchet
56 # should have a chance of getting a 4.2 release, and when we ratchet
57 # the version requirement forward again hopefully everyone will get
57 # the version requirement forward again hopefully everyone will get
58 # something that works for them.
58 # something that works for them.
59 if sys.version_info < (2, 7, 0, 'final'):
59 if sys.version_info < (2, 7, 0, 'final'):
60 pip_message = (
60 pip_message = (
61 'This may be due to an out of date pip. '
61 'This may be due to an out of date pip. '
62 'Make sure you have pip >= 9.0.1.'
62 'Make sure you have pip >= 9.0.1.'
63 )
63 )
64 try:
64 try:
65 import pip
65 import pip
66
66
67 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
67 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
68 if pip_version < (9, 0, 1):
68 if pip_version < (9, 0, 1):
69 pip_message = (
69 pip_message = (
70 'Your pip version is out of date, please install '
70 'Your pip version is out of date, please install '
71 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
71 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
72 )
72 )
73 else:
73 else:
74 # pip is new enough - it must be something else
74 # pip is new enough - it must be something else
75 pip_message = ''
75 pip_message = ''
76 except Exception:
76 except Exception:
77 pass
77 pass
78 error = """
78 error = """
79 Mercurial does not support Python older than 2.7.
79 Mercurial does not support Python older than 2.7.
80 Python {py} detected.
80 Python {py} detected.
81 {pip}
81 {pip}
82 """.format(
82 """.format(
83 py=sys.version_info, pip=pip_message
83 py=sys.version_info, pip=pip_message
84 )
84 )
85 printf(error, file=sys.stderr)
85 printf(error, file=sys.stderr)
86 sys.exit(1)
86 sys.exit(1)
87
87
88 if sys.version_info[0] >= 3:
88 if sys.version_info[0] >= 3:
89 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
89 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
90 else:
90 else:
91 # deprecated in Python 3
91 # deprecated in Python 3
92 DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
92 DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
93
93
94 # Solaris Python packaging brain damage
94 # Solaris Python packaging brain damage
95 try:
95 try:
96 import hashlib
96 import hashlib
97
97
98 sha = hashlib.sha1()
98 sha = hashlib.sha1()
99 except ImportError:
99 except ImportError:
100 try:
100 try:
101 import sha
101 import sha
102
102
103 sha.sha # silence unused import warning
103 sha.sha # silence unused import warning
104 except ImportError:
104 except ImportError:
105 raise SystemExit(
105 raise SystemExit(
106 "Couldn't import standard hashlib (incomplete Python install)."
106 "Couldn't import standard hashlib (incomplete Python install)."
107 )
107 )
108
108
109 try:
109 try:
110 import zlib
110 import zlib
111
111
112 zlib.compressobj # silence unused import warning
112 zlib.compressobj # silence unused import warning
113 except ImportError:
113 except ImportError:
114 raise SystemExit(
114 raise SystemExit(
115 "Couldn't import standard zlib (incomplete Python install)."
115 "Couldn't import standard zlib (incomplete Python install)."
116 )
116 )
117
117
118 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
118 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
119 isironpython = False
119 isironpython = False
120 try:
120 try:
121 isironpython = (
121 isironpython = (
122 platform.python_implementation().lower().find("ironpython") != -1
122 platform.python_implementation().lower().find("ironpython") != -1
123 )
123 )
124 except AttributeError:
124 except AttributeError:
125 pass
125 pass
126
126
127 if isironpython:
127 if isironpython:
128 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
128 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
129 else:
129 else:
130 try:
130 try:
131 import bz2
131 import bz2
132
132
133 bz2.BZ2Compressor # silence unused import warning
133 bz2.BZ2Compressor # silence unused import warning
134 except ImportError:
134 except ImportError:
135 raise SystemExit(
135 raise SystemExit(
136 "Couldn't import standard bz2 (incomplete Python install)."
136 "Couldn't import standard bz2 (incomplete Python install)."
137 )
137 )
138
138
139 ispypy = "PyPy" in sys.version
139 ispypy = "PyPy" in sys.version
140
140
141 hgrustext = os.environ.get('HGWITHRUSTEXT')
141 hgrustext = os.environ.get('HGWITHRUSTEXT')
142 # TODO record it for proper rebuild upon changes
142 # TODO record it for proper rebuild upon changes
143 # (see mercurial/__modulepolicy__.py)
143 # (see mercurial/__modulepolicy__.py)
144 if hgrustext != 'cpython' and hgrustext is not None:
144 if hgrustext != 'cpython' and hgrustext is not None:
145 if hgrustext:
145 if hgrustext:
146 print('unkown HGWITHRUSTEXT value: %s' % hgrustext, file=sys.stderr)
146 print('unkown HGWITHRUSTEXT value: %s' % hgrustext, file=sys.stderr)
147 hgrustext = None
147 hgrustext = None
148
148
149 import ctypes
149 import ctypes
150 import errno
150 import errno
151 import stat, subprocess, time
151 import stat, subprocess, time
152 import re
152 import re
153 import shutil
153 import shutil
154 import tempfile
154 import tempfile
155 from distutils import log
155 from distutils import log
156
156
157 # We have issues with setuptools on some platforms and builders. Until
157 # We have issues with setuptools on some platforms and builders. Until
158 # those are resolved, setuptools is opt-in except for platforms where
158 # those are resolved, setuptools is opt-in except for platforms where
159 # we don't have issues.
159 # we don't have issues.
160 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
160 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
161 if issetuptools:
161 if issetuptools:
162 from setuptools import setup
162 from setuptools import setup
163 else:
163 else:
164 from distutils.core import setup
164 from distutils.core import setup
165 from distutils.ccompiler import new_compiler
165 from distutils.ccompiler import new_compiler
166 from distutils.core import Command, Extension
166 from distutils.core import Command, Extension
167 from distutils.dist import Distribution
167 from distutils.dist import Distribution
168 from distutils.command.build import build
168 from distutils.command.build import build
169 from distutils.command.build_ext import build_ext
169 from distutils.command.build_ext import build_ext
170 from distutils.command.build_py import build_py
170 from distutils.command.build_py import build_py
171 from distutils.command.build_scripts import build_scripts
171 from distutils.command.build_scripts import build_scripts
172 from distutils.command.install import install
172 from distutils.command.install import install
173 from distutils.command.install_lib import install_lib
173 from distutils.command.install_lib import install_lib
174 from distutils.command.install_scripts import install_scripts
174 from distutils.command.install_scripts import install_scripts
175 from distutils.spawn import spawn, find_executable
175 from distutils.spawn import spawn, find_executable
176 from distutils import file_util
176 from distutils import file_util
177 from distutils.errors import (
177 from distutils.errors import (
178 CCompilerError,
178 CCompilerError,
179 DistutilsError,
179 DistutilsError,
180 DistutilsExecError,
180 DistutilsExecError,
181 )
181 )
182 from distutils.sysconfig import get_python_inc, get_config_var
182 from distutils.sysconfig import get_python_inc, get_config_var
183 from distutils.version import StrictVersion
183 from distutils.version import StrictVersion
184
184
185 # Explain to distutils.StrictVersion how our release candidates are versionned
185 # Explain to distutils.StrictVersion how our release candidates are versionned
186 StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')
186 StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')
187
187
188
188
189 def write_if_changed(path, content):
189 def write_if_changed(path, content):
190 """Write content to a file iff the content hasn't changed."""
190 """Write content to a file iff the content hasn't changed."""
191 if os.path.exists(path):
191 if os.path.exists(path):
192 with open(path, 'rb') as fh:
192 with open(path, 'rb') as fh:
193 current = fh.read()
193 current = fh.read()
194 else:
194 else:
195 current = b''
195 current = b''
196
196
197 if current != content:
197 if current != content:
198 with open(path, 'wb') as fh:
198 with open(path, 'wb') as fh:
199 fh.write(content)
199 fh.write(content)
200
200
201
201
202 scripts = ['hg']
202 scripts = ['hg']
203 if os.name == 'nt':
203 if os.name == 'nt':
204 # We remove hg.bat if we are able to build hg.exe.
204 # We remove hg.bat if we are able to build hg.exe.
205 scripts.append('contrib/win32/hg.bat')
205 scripts.append('contrib/win32/hg.bat')
206
206
207
207
208 def cancompile(cc, code):
208 def cancompile(cc, code):
209 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
209 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
210 devnull = oldstderr = None
210 devnull = oldstderr = None
211 try:
211 try:
212 fname = os.path.join(tmpdir, 'testcomp.c')
212 fname = os.path.join(tmpdir, 'testcomp.c')
213 f = open(fname, 'w')
213 f = open(fname, 'w')
214 f.write(code)
214 f.write(code)
215 f.close()
215 f.close()
216 # Redirect stderr to /dev/null to hide any error messages
216 # Redirect stderr to /dev/null to hide any error messages
217 # from the compiler.
217 # from the compiler.
218 # This will have to be changed if we ever have to check
218 # This will have to be changed if we ever have to check
219 # for a function on Windows.
219 # for a function on Windows.
220 devnull = open('/dev/null', 'w')
220 devnull = open('/dev/null', 'w')
221 oldstderr = os.dup(sys.stderr.fileno())
221 oldstderr = os.dup(sys.stderr.fileno())
222 os.dup2(devnull.fileno(), sys.stderr.fileno())
222 os.dup2(devnull.fileno(), sys.stderr.fileno())
223 objects = cc.compile([fname], output_dir=tmpdir)
223 objects = cc.compile([fname], output_dir=tmpdir)
224 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
224 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
225 return True
225 return True
226 except Exception:
226 except Exception:
227 return False
227 return False
228 finally:
228 finally:
229 if oldstderr is not None:
229 if oldstderr is not None:
230 os.dup2(oldstderr, sys.stderr.fileno())
230 os.dup2(oldstderr, sys.stderr.fileno())
231 if devnull is not None:
231 if devnull is not None:
232 devnull.close()
232 devnull.close()
233 shutil.rmtree(tmpdir)
233 shutil.rmtree(tmpdir)
234
234
235
235
236 # simplified version of distutils.ccompiler.CCompiler.has_function
236 # simplified version of distutils.ccompiler.CCompiler.has_function
237 # that actually removes its temporary files.
237 # that actually removes its temporary files.
238 def hasfunction(cc, funcname):
238 def hasfunction(cc, funcname):
239 code = 'int main(void) { %s(); }\n' % funcname
239 code = 'int main(void) { %s(); }\n' % funcname
240 return cancompile(cc, code)
240 return cancompile(cc, code)
241
241
242
242
243 def hasheader(cc, headername):
243 def hasheader(cc, headername):
244 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
244 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
245 return cancompile(cc, code)
245 return cancompile(cc, code)
246
246
247
247
248 # py2exe needs to be installed to work
248 # py2exe needs to be installed to work
249 try:
249 try:
250 import py2exe
250 import py2exe
251
251
252 py2exe.Distribution # silence unused import warning
252 py2exe.Distribution # silence unused import warning
253 py2exeloaded = True
253 py2exeloaded = True
254 # import py2exe's patched Distribution class
254 # import py2exe's patched Distribution class
255 from distutils.core import Distribution
255 from distutils.core import Distribution
256 except ImportError:
256 except ImportError:
257 py2exeloaded = False
257 py2exeloaded = False
258
258
259
259
260 def runcmd(cmd, env, cwd=None):
260 def runcmd(cmd, env, cwd=None):
261 p = subprocess.Popen(
261 p = subprocess.Popen(
262 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
262 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
263 )
263 )
264 out, err = p.communicate()
264 out, err = p.communicate()
265 return p.returncode, out, err
265 return p.returncode, out, err
266
266
267
267
268 class hgcommand(object):
268 class hgcommand(object):
269 def __init__(self, cmd, env):
269 def __init__(self, cmd, env):
270 self.cmd = cmd
270 self.cmd = cmd
271 self.env = env
271 self.env = env
272
272
273 def run(self, args):
273 def run(self, args):
274 cmd = self.cmd + args
274 cmd = self.cmd + args
275 returncode, out, err = runcmd(cmd, self.env)
275 returncode, out, err = runcmd(cmd, self.env)
276 err = filterhgerr(err)
276 err = filterhgerr(err)
277 if err or returncode != 0:
277 if err or returncode != 0:
278 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
278 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
279 printf(err, file=sys.stderr)
279 printf(err, file=sys.stderr)
280 return ''
280 return ''
281 return out
281 return out
282
282
283
283
284 def filterhgerr(err):
284 def filterhgerr(err):
285 # If root is executing setup.py, but the repository is owned by
285 # If root is executing setup.py, but the repository is owned by
286 # another user (as in "sudo python setup.py install") we will get
286 # another user (as in "sudo python setup.py install") we will get
287 # trust warnings since the .hg/hgrc file is untrusted. That is
287 # trust warnings since the .hg/hgrc file is untrusted. That is
288 # fine, we don't want to load it anyway. Python may warn about
288 # fine, we don't want to load it anyway. Python may warn about
289 # a missing __init__.py in mercurial/locale, we also ignore that.
289 # a missing __init__.py in mercurial/locale, we also ignore that.
290 err = [
290 err = [
291 e
291 e
292 for e in err.splitlines()
292 for e in err.splitlines()
293 if (
293 if (
294 not e.startswith(b'not trusting file')
294 not e.startswith(b'not trusting file')
295 and not e.startswith(b'warning: Not importing')
295 and not e.startswith(b'warning: Not importing')
296 and not e.startswith(b'obsolete feature not enabled')
296 and not e.startswith(b'obsolete feature not enabled')
297 and not e.startswith(b'*** failed to import extension')
297 and not e.startswith(b'*** failed to import extension')
298 and not e.startswith(b'devel-warn:')
298 and not e.startswith(b'devel-warn:')
299 and not (
299 and not (
300 e.startswith(b'(third party extension')
300 e.startswith(b'(third party extension')
301 and e.endswith(b'or newer of Mercurial; disabling)')
301 and e.endswith(b'or newer of Mercurial; disabling)')
302 )
302 )
303 )
303 )
304 ]
304 ]
305 return b'\n'.join(b' ' + e for e in err)
305 return b'\n'.join(b' ' + e for e in err)
306
306
307
307
308 def findhg():
308 def findhg():
309 """Try to figure out how we should invoke hg for examining the local
309 """Try to figure out how we should invoke hg for examining the local
310 repository contents.
310 repository contents.
311
311
312 Returns an hgcommand object."""
312 Returns an hgcommand object."""
313 # By default, prefer the "hg" command in the user's path. This was
313 # By default, prefer the "hg" command in the user's path. This was
314 # presumably the hg command that the user used to create this repository.
314 # presumably the hg command that the user used to create this repository.
315 #
315 #
316 # This repository may require extensions or other settings that would not
316 # This repository may require extensions or other settings that would not
317 # be enabled by running the hg script directly from this local repository.
317 # be enabled by running the hg script directly from this local repository.
318 hgenv = os.environ.copy()
318 hgenv = os.environ.copy()
319 # Use HGPLAIN to disable hgrc settings that would change output formatting,
319 # Use HGPLAIN to disable hgrc settings that would change output formatting,
320 # and disable localization for the same reasons.
320 # and disable localization for the same reasons.
321 hgenv['HGPLAIN'] = '1'
321 hgenv['HGPLAIN'] = '1'
322 hgenv['LANGUAGE'] = 'C'
322 hgenv['LANGUAGE'] = 'C'
323 hgcmd = ['hg']
323 hgcmd = ['hg']
324 # Run a simple "hg log" command just to see if using hg from the user's
324 # Run a simple "hg log" command just to see if using hg from the user's
325 # path works and can successfully interact with this repository. Windows
325 # path works and can successfully interact with this repository. Windows
326 # gives precedence to hg.exe in the current directory, so fall back to the
326 # gives precedence to hg.exe in the current directory, so fall back to the
327 # python invocation of local hg, where pythonXY.dll can always be found.
327 # python invocation of local hg, where pythonXY.dll can always be found.
328 check_cmd = ['log', '-r.', '-Ttest']
328 check_cmd = ['log', '-r.', '-Ttest']
329 if os.name != 'nt' or not os.path.exists("hg.exe"):
329 if os.name != 'nt' or not os.path.exists("hg.exe"):
330 try:
330 try:
331 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
331 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
332 except EnvironmentError:
332 except EnvironmentError:
333 retcode = -1
333 retcode = -1
334 if retcode == 0 and not filterhgerr(err):
334 if retcode == 0 and not filterhgerr(err):
335 return hgcommand(hgcmd, hgenv)
335 return hgcommand(hgcmd, hgenv)
336
336
337 # Fall back to trying the local hg installation.
337 # Fall back to trying the local hg installation.
338 hgenv = localhgenv()
338 hgenv = localhgenv()
339 hgcmd = [sys.executable, 'hg']
339 hgcmd = [sys.executable, 'hg']
340 try:
340 try:
341 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
341 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
342 except EnvironmentError:
342 except EnvironmentError:
343 retcode = -1
343 retcode = -1
344 if retcode == 0 and not filterhgerr(err):
344 if retcode == 0 and not filterhgerr(err):
345 return hgcommand(hgcmd, hgenv)
345 return hgcommand(hgcmd, hgenv)
346
346
347 raise SystemExit(
347 raise SystemExit(
348 'Unable to find a working hg binary to extract the '
348 'Unable to find a working hg binary to extract the '
349 'version from the repository tags'
349 'version from the repository tags'
350 )
350 )
351
351
352
352
353 def localhgenv():
353 def localhgenv():
354 """Get an environment dictionary to use for invoking or importing
354 """Get an environment dictionary to use for invoking or importing
355 mercurial from the local repository."""
355 mercurial from the local repository."""
356 # Execute hg out of this directory with a custom environment which takes
356 # Execute hg out of this directory with a custom environment which takes
357 # care to not use any hgrc files and do no localization.
357 # care to not use any hgrc files and do no localization.
358 env = {
358 env = {
359 'HGMODULEPOLICY': 'py',
359 'HGMODULEPOLICY': 'py',
360 'HGRCPATH': '',
360 'HGRCPATH': '',
361 'LANGUAGE': 'C',
361 'LANGUAGE': 'C',
362 'PATH': '',
362 'PATH': '',
363 } # make pypi modules that use os.environ['PATH'] happy
363 } # make pypi modules that use os.environ['PATH'] happy
364 if 'LD_LIBRARY_PATH' in os.environ:
364 if 'LD_LIBRARY_PATH' in os.environ:
365 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
365 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
366 if 'SystemRoot' in os.environ:
366 if 'SystemRoot' in os.environ:
367 # SystemRoot is required by Windows to load various DLLs. See:
367 # SystemRoot is required by Windows to load various DLLs. See:
368 # https://bugs.python.org/issue13524#msg148850
368 # https://bugs.python.org/issue13524#msg148850
369 env['SystemRoot'] = os.environ['SystemRoot']
369 env['SystemRoot'] = os.environ['SystemRoot']
370 return env
370 return env
371
371
372
372
373 version = ''
373 version = ''
374
374
375 if os.path.isdir('.hg'):
375 if os.path.isdir('.hg'):
376 hg = findhg()
376 hg = findhg()
377 cmd = ['log', '-r', '.', '--template', '{tags}\n']
377 cmd = ['log', '-r', '.', '--template', '{tags}\n']
378 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
378 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
379 hgid = sysstr(hg.run(['id', '-i'])).strip()
379 hgid = sysstr(hg.run(['id', '-i'])).strip()
380 if not hgid:
380 if not hgid:
381 # Bail out if hg is having problems interacting with this repository,
381 # Bail out if hg is having problems interacting with this repository,
382 # rather than falling through and producing a bogus version number.
382 # rather than falling through and producing a bogus version number.
383 # Continuing with an invalid version number will break extensions
383 # Continuing with an invalid version number will break extensions
384 # that define minimumhgversion.
384 # that define minimumhgversion.
385 raise SystemExit('Unable to determine hg version from local repository')
385 raise SystemExit('Unable to determine hg version from local repository')
386 if numerictags: # tag(s) found
386 if numerictags: # tag(s) found
387 version = numerictags[-1]
387 version = numerictags[-1]
388 if hgid.endswith('+'): # propagate the dirty status to the tag
388 if hgid.endswith('+'): # propagate the dirty status to the tag
389 version += '+'
389 version += '+'
390 else: # no tag found
390 else: # no tag found
391 ltagcmd = ['parents', '--template', '{latesttag}']
391 ltagcmd = ['parents', '--template', '{latesttag}']
392 ltag = sysstr(hg.run(ltagcmd))
392 ltag = sysstr(hg.run(ltagcmd))
393 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
393 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
394 changessince = len(hg.run(changessincecmd).splitlines())
394 changessince = len(hg.run(changessincecmd).splitlines())
395 version = '%s+%s-%s' % (ltag, changessince, hgid)
395 version = '%s+%s-%s' % (ltag, changessince, hgid)
396 if version.endswith('+'):
396 if version.endswith('+'):
397 version += time.strftime('%Y%m%d')
397 version += time.strftime('%Y%m%d')
398 elif os.path.exists('.hg_archival.txt'):
398 elif os.path.exists('.hg_archival.txt'):
399 kw = dict(
399 kw = dict(
400 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
400 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
401 )
401 )
402 if 'tag' in kw:
402 if 'tag' in kw:
403 version = kw['tag']
403 version = kw['tag']
404 elif 'latesttag' in kw:
404 elif 'latesttag' in kw:
405 if 'changessincelatesttag' in kw:
405 if 'changessincelatesttag' in kw:
406 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
406 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
407 else:
407 else:
408 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
408 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
409 else:
409 else:
410 version = kw.get('node', '')[:12]
410 version = kw.get('node', '')[:12]
411
411
412 if version:
412 if version:
413 versionb = version
413 versionb = version
414 if not isinstance(versionb, bytes):
414 if not isinstance(versionb, bytes):
415 versionb = versionb.encode('ascii')
415 versionb = versionb.encode('ascii')
416
416
417 write_if_changed(
417 write_if_changed(
418 'mercurial/__version__.py',
418 'mercurial/__version__.py',
419 b''.join(
419 b''.join(
420 [
420 [
421 b'# this file is autogenerated by setup.py\n'
421 b'# this file is autogenerated by setup.py\n'
422 b'version = b"%s"\n' % versionb,
422 b'version = b"%s"\n' % versionb,
423 ]
423 ]
424 ),
424 ),
425 )
425 )
426
426
427 try:
427 try:
428 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
428 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
429 os.environ['HGMODULEPOLICY'] = 'py'
429 os.environ['HGMODULEPOLICY'] = 'py'
430 from mercurial import __version__
430 from mercurial import __version__
431
431
432 version = __version__.version
432 version = __version__.version
433 except ImportError:
433 except ImportError:
434 version = b'unknown'
434 version = b'unknown'
435 finally:
435 finally:
436 if oldpolicy is None:
436 if oldpolicy is None:
437 del os.environ['HGMODULEPOLICY']
437 del os.environ['HGMODULEPOLICY']
438 else:
438 else:
439 os.environ['HGMODULEPOLICY'] = oldpolicy
439 os.environ['HGMODULEPOLICY'] = oldpolicy
440
440
441
441
442 class hgbuild(build):
442 class hgbuild(build):
443 # Insert hgbuildmo first so that files in mercurial/locale/ are found
443 # Insert hgbuildmo first so that files in mercurial/locale/ are found
444 # when build_py is run next.
444 # when build_py is run next.
445 sub_commands = [('build_mo', None)] + build.sub_commands
445 sub_commands = [('build_mo', None)] + build.sub_commands
446
446
447
447
448 class hgbuildmo(build):
448 class hgbuildmo(build):
449
449
450 description = "build translations (.mo files)"
450 description = "build translations (.mo files)"
451
451
452 def run(self):
452 def run(self):
453 if not find_executable('msgfmt'):
453 if not find_executable('msgfmt'):
454 self.warn(
454 self.warn(
455 "could not find msgfmt executable, no translations "
455 "could not find msgfmt executable, no translations "
456 "will be built"
456 "will be built"
457 )
457 )
458 return
458 return
459
459
460 podir = 'i18n'
460 podir = 'i18n'
461 if not os.path.isdir(podir):
461 if not os.path.isdir(podir):
462 self.warn("could not find %s/ directory" % podir)
462 self.warn("could not find %s/ directory" % podir)
463 return
463 return
464
464
465 join = os.path.join
465 join = os.path.join
466 for po in os.listdir(podir):
466 for po in os.listdir(podir):
467 if not po.endswith('.po'):
467 if not po.endswith('.po'):
468 continue
468 continue
469 pofile = join(podir, po)
469 pofile = join(podir, po)
470 modir = join('locale', po[:-3], 'LC_MESSAGES')
470 modir = join('locale', po[:-3], 'LC_MESSAGES')
471 mofile = join(modir, 'hg.mo')
471 mofile = join(modir, 'hg.mo')
472 mobuildfile = join('mercurial', mofile)
472 mobuildfile = join('mercurial', mofile)
473 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
473 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
474 if sys.platform != 'sunos5':
474 if sys.platform != 'sunos5':
475 # msgfmt on Solaris does not know about -c
475 # msgfmt on Solaris does not know about -c
476 cmd.append('-c')
476 cmd.append('-c')
477 self.mkpath(join('mercurial', modir))
477 self.mkpath(join('mercurial', modir))
478 self.make_file([pofile], mobuildfile, spawn, (cmd,))
478 self.make_file([pofile], mobuildfile, spawn, (cmd,))
479
479
480
480
481 class hgdist(Distribution):
481 class hgdist(Distribution):
482 pure = False
482 pure = False
483 rust = hgrustext is not None
483 rust = hgrustext is not None
484 cffi = ispypy
484 cffi = ispypy
485
485
486 global_options = Distribution.global_options + [
486 global_options = Distribution.global_options + [
487 ('pure', None, "use pure (slow) Python code instead of C extensions"),
487 ('pure', None, "use pure (slow) Python code instead of C extensions"),
488 ('rust', None, "use Rust extensions additionally to C extensions"),
488 ('rust', None, "use Rust extensions additionally to C extensions"),
489 ]
489 ]
490
490
491 def has_ext_modules(self):
491 def has_ext_modules(self):
492 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
492 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
493 # too late for some cases
493 # too late for some cases
494 return not self.pure and Distribution.has_ext_modules(self)
494 return not self.pure and Distribution.has_ext_modules(self)
495
495
496
496
497 # This is ugly as a one-liner. So use a variable.
497 # This is ugly as a one-liner. So use a variable.
498 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
498 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
499 buildextnegops['no-zstd'] = 'zstd'
499 buildextnegops['no-zstd'] = 'zstd'
500 buildextnegops['no-rust'] = 'rust'
500 buildextnegops['no-rust'] = 'rust'
501
501
502
502
503 class hgbuildext(build_ext):
503 class hgbuildext(build_ext):
504 user_options = build_ext.user_options + [
504 user_options = build_ext.user_options + [
505 ('zstd', None, 'compile zstd bindings [default]'),
505 ('zstd', None, 'compile zstd bindings [default]'),
506 ('no-zstd', None, 'do not compile zstd bindings'),
506 ('no-zstd', None, 'do not compile zstd bindings'),
507 (
507 (
508 'rust',
508 'rust',
509 None,
509 None,
510 'compile Rust extensions if they are in use '
510 'compile Rust extensions if they are in use '
511 '(requires Cargo) [default]',
511 '(requires Cargo) [default]',
512 ),
512 ),
513 ('no-rust', None, 'do not compile Rust extensions'),
513 ('no-rust', None, 'do not compile Rust extensions'),
514 ]
514 ]
515
515
516 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
516 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
517 negative_opt = buildextnegops
517 negative_opt = buildextnegops
518
518
519 def initialize_options(self):
519 def initialize_options(self):
520 self.zstd = True
520 self.zstd = True
521 self.rust = True
521 self.rust = True
522
522
523 return build_ext.initialize_options(self)
523 return build_ext.initialize_options(self)
524
524
525 def finalize_options(self):
525 def finalize_options(self):
526 # Unless overridden by the end user, build extensions in parallel.
526 # Unless overridden by the end user, build extensions in parallel.
527 # Only influences behavior on Python 3.5+.
527 # Only influences behavior on Python 3.5+.
528 if getattr(self, 'parallel', None) is None:
528 if getattr(self, 'parallel', None) is None:
529 self.parallel = True
529 self.parallel = True
530
530
531 return build_ext.finalize_options(self)
531 return build_ext.finalize_options(self)
532
532
533 def build_extensions(self):
533 def build_extensions(self):
534 ruststandalones = [
534 ruststandalones = [
535 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
535 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
536 ]
536 ]
537 self.extensions = [
537 self.extensions = [
538 e for e in self.extensions if e not in ruststandalones
538 e for e in self.extensions if e not in ruststandalones
539 ]
539 ]
540 # Filter out zstd if disabled via argument.
540 # Filter out zstd if disabled via argument.
541 if not self.zstd:
541 if not self.zstd:
542 self.extensions = [
542 self.extensions = [
543 e for e in self.extensions if e.name != 'mercurial.zstd'
543 e for e in self.extensions if e.name != 'mercurial.zstd'
544 ]
544 ]
545
545
546 # Build Rust standalon extensions if it'll be used
546 # Build Rust standalon extensions if it'll be used
547 # and its build is not explictely disabled (for external build
547 # and its build is not explictely disabled (for external build
548 # as Linux distributions would do)
548 # as Linux distributions would do)
549 if self.distribution.rust and self.rust:
549 if self.distribution.rust and self.rust:
550 for rustext in ruststandalones:
550 for rustext in ruststandalones:
551 rustext.build('' if self.inplace else self.build_lib)
551 rustext.build('' if self.inplace else self.build_lib)
552
552
553 return build_ext.build_extensions(self)
553 return build_ext.build_extensions(self)
554
554
555 def build_extension(self, ext):
555 def build_extension(self, ext):
556 if (
556 if (
557 self.distribution.rust
557 self.distribution.rust
558 and self.rust
558 and self.rust
559 and isinstance(ext, RustExtension)
559 and isinstance(ext, RustExtension)
560 ):
560 ):
561 ext.rustbuild()
561 ext.rustbuild()
562 try:
562 try:
563 build_ext.build_extension(self, ext)
563 build_ext.build_extension(self, ext)
564 except CCompilerError:
564 except CCompilerError:
565 if not getattr(ext, 'optional', False):
565 if not getattr(ext, 'optional', False):
566 raise
566 raise
567 log.warn(
567 log.warn(
568 "Failed to build optional extension '%s' (skipping)", ext.name
568 "Failed to build optional extension '%s' (skipping)", ext.name
569 )
569 )
570
570
571
571
572 class hgbuildscripts(build_scripts):
572 class hgbuildscripts(build_scripts):
573 def run(self):
573 def run(self):
574 if os.name != 'nt' or self.distribution.pure:
574 if os.name != 'nt' or self.distribution.pure:
575 return build_scripts.run(self)
575 return build_scripts.run(self)
576
576
577 exebuilt = False
577 exebuilt = False
578 try:
578 try:
579 self.run_command('build_hgexe')
579 self.run_command('build_hgexe')
580 exebuilt = True
580 exebuilt = True
581 except (DistutilsError, CCompilerError):
581 except (DistutilsError, CCompilerError):
582 log.warn('failed to build optional hg.exe')
582 log.warn('failed to build optional hg.exe')
583
583
584 if exebuilt:
584 if exebuilt:
585 # Copying hg.exe to the scripts build directory ensures it is
585 # Copying hg.exe to the scripts build directory ensures it is
586 # installed by the install_scripts command.
586 # installed by the install_scripts command.
587 hgexecommand = self.get_finalized_command('build_hgexe')
587 hgexecommand = self.get_finalized_command('build_hgexe')
588 dest = os.path.join(self.build_dir, 'hg.exe')
588 dest = os.path.join(self.build_dir, 'hg.exe')
589 self.mkpath(self.build_dir)
589 self.mkpath(self.build_dir)
590 self.copy_file(hgexecommand.hgexepath, dest)
590 self.copy_file(hgexecommand.hgexepath, dest)
591
591
592 # Remove hg.bat because it is redundant with hg.exe.
592 # Remove hg.bat because it is redundant with hg.exe.
593 self.scripts.remove('contrib/win32/hg.bat')
593 self.scripts.remove('contrib/win32/hg.bat')
594
594
595 return build_scripts.run(self)
595 return build_scripts.run(self)
596
596
597
597
598 class hgbuildpy(build_py):
598 class hgbuildpy(build_py):
599 def finalize_options(self):
599 def finalize_options(self):
600 build_py.finalize_options(self)
600 build_py.finalize_options(self)
601
601
602 if self.distribution.pure:
602 if self.distribution.pure:
603 self.distribution.ext_modules = []
603 self.distribution.ext_modules = []
604 elif self.distribution.cffi:
604 elif self.distribution.cffi:
605 from mercurial.cffi import (
605 from mercurial.cffi import (
606 bdiffbuild,
606 bdiffbuild,
607 mpatchbuild,
607 mpatchbuild,
608 )
608 )
609
609
610 exts = [
610 exts = [
611 mpatchbuild.ffi.distutils_extension(),
611 mpatchbuild.ffi.distutils_extension(),
612 bdiffbuild.ffi.distutils_extension(),
612 bdiffbuild.ffi.distutils_extension(),
613 ]
613 ]
614 # cffi modules go here
614 # cffi modules go here
615 if sys.platform == 'darwin':
615 if sys.platform == 'darwin':
616 from mercurial.cffi import osutilbuild
616 from mercurial.cffi import osutilbuild
617
617
618 exts.append(osutilbuild.ffi.distutils_extension())
618 exts.append(osutilbuild.ffi.distutils_extension())
619 self.distribution.ext_modules = exts
619 self.distribution.ext_modules = exts
620 else:
620 else:
621 h = os.path.join(get_python_inc(), 'Python.h')
621 h = os.path.join(get_python_inc(), 'Python.h')
622 if not os.path.exists(h):
622 if not os.path.exists(h):
623 raise SystemExit(
623 raise SystemExit(
624 'Python headers are required to build '
624 'Python headers are required to build '
625 'Mercurial but weren\'t found in %s' % h
625 'Mercurial but weren\'t found in %s' % h
626 )
626 )
627
627
628 def run(self):
628 def run(self):
629 basepath = os.path.join(self.build_lib, 'mercurial')
629 basepath = os.path.join(self.build_lib, 'mercurial')
630 self.mkpath(basepath)
630 self.mkpath(basepath)
631
631
632 rust = self.distribution.rust
632 rust = self.distribution.rust
633 if self.distribution.pure:
633 if self.distribution.pure:
634 modulepolicy = 'py'
634 modulepolicy = 'py'
635 elif self.build_lib == '.':
635 elif self.build_lib == '.':
636 # in-place build should run without rebuilding and Rust extensions
636 # in-place build should run without rebuilding and Rust extensions
637 modulepolicy = 'rust+c-allow' if rust else 'allow'
637 modulepolicy = 'rust+c-allow' if rust else 'allow'
638 else:
638 else:
639 modulepolicy = 'rust+c' if rust else 'c'
639 modulepolicy = 'rust+c' if rust else 'c'
640
640
641 content = b''.join(
641 content = b''.join(
642 [
642 [
643 b'# this file is autogenerated by setup.py\n',
643 b'# this file is autogenerated by setup.py\n',
644 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
644 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
645 ]
645 ]
646 )
646 )
647 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
647 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
648
648
649 build_py.run(self)
649 build_py.run(self)
650
650
651
651
652 class buildhgextindex(Command):
652 class buildhgextindex(Command):
653 description = 'generate prebuilt index of hgext (for frozen package)'
653 description = 'generate prebuilt index of hgext (for frozen package)'
654 user_options = []
654 user_options = []
655 _indexfilename = 'hgext/__index__.py'
655 _indexfilename = 'hgext/__index__.py'
656
656
657 def initialize_options(self):
657 def initialize_options(self):
658 pass
658 pass
659
659
660 def finalize_options(self):
660 def finalize_options(self):
661 pass
661 pass
662
662
663 def run(self):
663 def run(self):
664 if os.path.exists(self._indexfilename):
664 if os.path.exists(self._indexfilename):
665 with open(self._indexfilename, 'w') as f:
665 with open(self._indexfilename, 'w') as f:
666 f.write('# empty\n')
666 f.write('# empty\n')
667
667
668 # here no extension enabled, disabled() lists up everything
668 # here no extension enabled, disabled() lists up everything
669 code = (
669 code = (
670 'import pprint; from mercurial import extensions; '
670 'import pprint; from mercurial import extensions; '
671 'ext = extensions.disabled();'
671 'ext = extensions.disabled();'
672 'ext.pop("__index__", None);'
672 'ext.pop("__index__", None);'
673 'pprint.pprint(ext)'
673 'pprint.pprint(ext)'
674 )
674 )
675 returncode, out, err = runcmd(
675 returncode, out, err = runcmd(
676 [sys.executable, '-c', code], localhgenv()
676 [sys.executable, '-c', code], localhgenv()
677 )
677 )
678 if err or returncode != 0:
678 if err or returncode != 0:
679 raise DistutilsExecError(err)
679 raise DistutilsExecError(err)
680
680
681 with open(self._indexfilename, 'wb') as f:
681 with open(self._indexfilename, 'wb') as f:
682 f.write(b'# this file is autogenerated by setup.py\n')
682 f.write(b'# this file is autogenerated by setup.py\n')
683 f.write(b'docs = ')
683 f.write(b'docs = ')
684 f.write(out)
684 f.write(out)
685
685
686
686
687 class buildhgexe(build_ext):
687 class buildhgexe(build_ext):
688 description = 'compile hg.exe from mercurial/exewrapper.c'
688 description = 'compile hg.exe from mercurial/exewrapper.c'
689 user_options = build_ext.user_options + [
689 user_options = build_ext.user_options + [
690 (
690 (
691 'long-paths-support',
691 'long-paths-support',
692 None,
692 None,
693 'enable support for long paths on '
693 'enable support for long paths on '
694 'Windows (off by default and '
694 'Windows (off by default and '
695 'experimental)',
695 'experimental)',
696 ),
696 ),
697 ]
697 ]
698
698
699 LONG_PATHS_MANIFEST = """
699 LONG_PATHS_MANIFEST = """
700 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
700 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
701 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
701 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
702 <application>
702 <application>
703 <windowsSettings
703 <windowsSettings
704 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
704 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
705 <ws2:longPathAware>true</ws2:longPathAware>
705 <ws2:longPathAware>true</ws2:longPathAware>
706 </windowsSettings>
706 </windowsSettings>
707 </application>
707 </application>
708 </assembly>"""
708 </assembly>"""
709
709
710 def initialize_options(self):
710 def initialize_options(self):
711 build_ext.initialize_options(self)
711 build_ext.initialize_options(self)
712 self.long_paths_support = False
712 self.long_paths_support = False
713
713
714 def build_extensions(self):
714 def build_extensions(self):
715 if os.name != 'nt':
715 if os.name != 'nt':
716 return
716 return
717 if isinstance(self.compiler, HackedMingw32CCompiler):
717 if isinstance(self.compiler, HackedMingw32CCompiler):
718 self.compiler.compiler_so = self.compiler.compiler # no -mdll
718 self.compiler.compiler_so = self.compiler.compiler # no -mdll
719 self.compiler.dll_libraries = [] # no -lmsrvc90
719 self.compiler.dll_libraries = [] # no -lmsrvc90
720
720
721 pythonlib = None
721 pythonlib = None
722
722
723 if getattr(sys, 'dllhandle', None):
723 if getattr(sys, 'dllhandle', None):
724 # Different Python installs can have different Python library
724 # Different Python installs can have different Python library
725 # names. e.g. the official CPython distribution uses pythonXY.dll
725 # names. e.g. the official CPython distribution uses pythonXY.dll
726 # and MinGW uses libpythonX.Y.dll.
726 # and MinGW uses libpythonX.Y.dll.
727 _kernel32 = ctypes.windll.kernel32
727 _kernel32 = ctypes.windll.kernel32
728 _kernel32.GetModuleFileNameA.argtypes = [
728 _kernel32.GetModuleFileNameA.argtypes = [
729 ctypes.c_void_p,
729 ctypes.c_void_p,
730 ctypes.c_void_p,
730 ctypes.c_void_p,
731 ctypes.c_ulong,
731 ctypes.c_ulong,
732 ]
732 ]
733 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
733 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
734 size = 1000
734 size = 1000
735 buf = ctypes.create_string_buffer(size + 1)
735 buf = ctypes.create_string_buffer(size + 1)
736 filelen = _kernel32.GetModuleFileNameA(
736 filelen = _kernel32.GetModuleFileNameA(
737 sys.dllhandle, ctypes.byref(buf), size
737 sys.dllhandle, ctypes.byref(buf), size
738 )
738 )
739
739
740 if filelen > 0 and filelen != size:
740 if filelen > 0 and filelen != size:
741 dllbasename = os.path.basename(buf.value)
741 dllbasename = os.path.basename(buf.value)
742 if not dllbasename.lower().endswith(b'.dll'):
742 if not dllbasename.lower().endswith(b'.dll'):
743 raise SystemExit(
743 raise SystemExit(
744 'Python DLL does not end with .dll: %s' % dllbasename
744 'Python DLL does not end with .dll: %s' % dllbasename
745 )
745 )
746 pythonlib = dllbasename[:-4]
746 pythonlib = dllbasename[:-4]
747
747
748 if not pythonlib:
748 if not pythonlib:
749 log.warn(
749 log.warn(
750 'could not determine Python DLL filename; assuming pythonXY'
750 'could not determine Python DLL filename; assuming pythonXY'
751 )
751 )
752
752
753 hv = sys.hexversion
753 hv = sys.hexversion
754 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
754 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
755
755
756 log.info('using %s as Python library name' % pythonlib)
756 log.info('using %s as Python library name' % pythonlib)
757 with open('mercurial/hgpythonlib.h', 'wb') as f:
757 with open('mercurial/hgpythonlib.h', 'wb') as f:
758 f.write(b'/* this file is autogenerated by setup.py */\n')
758 f.write(b'/* this file is autogenerated by setup.py */\n')
759 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
759 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
760
760
761 macros = None
761 macros = None
762 if sys.version_info[0] >= 3:
762 if sys.version_info[0] >= 3:
763 macros = [('_UNICODE', None), ('UNICODE', None)]
763 macros = [('_UNICODE', None), ('UNICODE', None)]
764
764
765 objects = self.compiler.compile(
765 objects = self.compiler.compile(
766 ['mercurial/exewrapper.c'],
766 ['mercurial/exewrapper.c'],
767 output_dir=self.build_temp,
767 output_dir=self.build_temp,
768 macros=macros,
768 macros=macros,
769 )
769 )
770 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
770 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
771 self.hgtarget = os.path.join(dir, 'hg')
771 self.hgtarget = os.path.join(dir, 'hg')
772 self.compiler.link_executable(
772 self.compiler.link_executable(
773 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
773 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
774 )
774 )
775 if self.long_paths_support:
775 if self.long_paths_support:
776 self.addlongpathsmanifest()
776 self.addlongpathsmanifest()
777
777
778 def addlongpathsmanifest(self):
778 def addlongpathsmanifest(self):
779 r"""Add manifest pieces so that hg.exe understands long paths
779 r"""Add manifest pieces so that hg.exe understands long paths
780
780
781 This is an EXPERIMENTAL feature, use with care.
781 This is an EXPERIMENTAL feature, use with care.
782 To enable long paths support, one needs to do two things:
782 To enable long paths support, one needs to do two things:
783 - build Mercurial with --long-paths-support option
783 - build Mercurial with --long-paths-support option
784 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
784 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
785 LongPathsEnabled to have value 1.
785 LongPathsEnabled to have value 1.
786
786
787 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
787 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
788 it happens because Mercurial uses mt.exe circa 2008, which is not
788 it happens because Mercurial uses mt.exe circa 2008, which is not
789 yet aware of long paths support in the manifest (I think so at least).
789 yet aware of long paths support in the manifest (I think so at least).
790 This does not stop mt.exe from embedding/merging the XML properly.
790 This does not stop mt.exe from embedding/merging the XML properly.
791
791
792 Why resource #1 should be used for .exe manifests? I don't know and
792 Why resource #1 should be used for .exe manifests? I don't know and
793 wasn't able to find an explanation for mortals. But it seems to work.
793 wasn't able to find an explanation for mortals. But it seems to work.
794 """
794 """
795 exefname = self.compiler.executable_filename(self.hgtarget)
795 exefname = self.compiler.executable_filename(self.hgtarget)
796 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
796 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
797 os.close(fdauto)
797 os.close(fdauto)
798 with open(manfname, 'w') as f:
798 with open(manfname, 'w') as f:
799 f.write(self.LONG_PATHS_MANIFEST)
799 f.write(self.LONG_PATHS_MANIFEST)
800 log.info("long paths manifest is written to '%s'" % manfname)
800 log.info("long paths manifest is written to '%s'" % manfname)
801 inputresource = '-inputresource:%s;#1' % exefname
801 inputresource = '-inputresource:%s;#1' % exefname
802 outputresource = '-outputresource:%s;#1' % exefname
802 outputresource = '-outputresource:%s;#1' % exefname
803 log.info("running mt.exe to update hg.exe's manifest in-place")
803 log.info("running mt.exe to update hg.exe's manifest in-place")
804 # supplying both -manifest and -inputresource to mt.exe makes
804 # supplying both -manifest and -inputresource to mt.exe makes
805 # it merge the embedded and supplied manifests in the -outputresource
805 # it merge the embedded and supplied manifests in the -outputresource
806 self.spawn(
806 self.spawn(
807 [
807 [
808 'mt.exe',
808 'mt.exe',
809 '-nologo',
809 '-nologo',
810 '-manifest',
810 '-manifest',
811 manfname,
811 manfname,
812 inputresource,
812 inputresource,
813 outputresource,
813 outputresource,
814 ]
814 ]
815 )
815 )
816 log.info("done updating hg.exe's manifest")
816 log.info("done updating hg.exe's manifest")
817 os.remove(manfname)
817 os.remove(manfname)
818
818
819 @property
819 @property
820 def hgexepath(self):
820 def hgexepath(self):
821 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
821 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
822 return os.path.join(self.build_temp, dir, 'hg.exe')
822 return os.path.join(self.build_temp, dir, 'hg.exe')
823
823
824
824
825 class hgbuilddoc(Command):
825 class hgbuilddoc(Command):
826 description = 'build documentation'
826 description = 'build documentation'
827 user_options = [
827 user_options = [
828 ('man', None, 'generate man pages'),
828 ('man', None, 'generate man pages'),
829 ('html', None, 'generate html pages'),
829 ('html', None, 'generate html pages'),
830 ]
830 ]
831
831
832 def initialize_options(self):
832 def initialize_options(self):
833 self.man = None
833 self.man = None
834 self.html = None
834 self.html = None
835
835
836 def finalize_options(self):
836 def finalize_options(self):
837 # If --man or --html are set, only generate what we're told to.
837 # If --man or --html are set, only generate what we're told to.
838 # Otherwise generate everything.
838 # Otherwise generate everything.
839 have_subset = self.man is not None or self.html is not None
839 have_subset = self.man is not None or self.html is not None
840
840
841 if have_subset:
841 if have_subset:
842 self.man = True if self.man else False
842 self.man = True if self.man else False
843 self.html = True if self.html else False
843 self.html = True if self.html else False
844 else:
844 else:
845 self.man = True
845 self.man = True
846 self.html = True
846 self.html = True
847
847
848 def run(self):
848 def run(self):
849 def normalizecrlf(p):
849 def normalizecrlf(p):
850 with open(p, 'rb') as fh:
850 with open(p, 'rb') as fh:
851 orig = fh.read()
851 orig = fh.read()
852
852
853 if b'\r\n' not in orig:
853 if b'\r\n' not in orig:
854 return
854 return
855
855
856 log.info('normalizing %s to LF line endings' % p)
856 log.info('normalizing %s to LF line endings' % p)
857 with open(p, 'wb') as fh:
857 with open(p, 'wb') as fh:
858 fh.write(orig.replace(b'\r\n', b'\n'))
858 fh.write(orig.replace(b'\r\n', b'\n'))
859
859
860 def gentxt(root):
860 def gentxt(root):
861 txt = 'doc/%s.txt' % root
861 txt = 'doc/%s.txt' % root
862 log.info('generating %s' % txt)
862 log.info('generating %s' % txt)
863 res, out, err = runcmd(
863 res, out, err = runcmd(
864 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
864 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
865 )
865 )
866 if res:
866 if res:
867 raise SystemExit(
867 raise SystemExit(
868 'error running gendoc.py: %s' % '\n'.join([out, err])
868 'error running gendoc.py: %s' % '\n'.join([out, err])
869 )
869 )
870
870
871 with open(txt, 'wb') as fh:
871 with open(txt, 'wb') as fh:
872 fh.write(out)
872 fh.write(out)
873
873
874 def gengendoc(root):
874 def gengendoc(root):
875 gendoc = 'doc/%s.gendoc.txt' % root
875 gendoc = 'doc/%s.gendoc.txt' % root
876
876
877 log.info('generating %s' % gendoc)
877 log.info('generating %s' % gendoc)
878 res, out, err = runcmd(
878 res, out, err = runcmd(
879 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
879 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
880 os.environ,
880 os.environ,
881 cwd='doc',
881 cwd='doc',
882 )
882 )
883 if res:
883 if res:
884 raise SystemExit(
884 raise SystemExit(
885 'error running gendoc: %s' % '\n'.join([out, err])
885 'error running gendoc: %s' % '\n'.join([out, err])
886 )
886 )
887
887
888 with open(gendoc, 'wb') as fh:
888 with open(gendoc, 'wb') as fh:
889 fh.write(out)
889 fh.write(out)
890
890
891 def genman(root):
891 def genman(root):
892 log.info('generating doc/%s' % root)
892 log.info('generating doc/%s' % root)
893 res, out, err = runcmd(
893 res, out, err = runcmd(
894 [
894 [
895 sys.executable,
895 sys.executable,
896 'runrst',
896 'runrst',
897 'hgmanpage',
897 'hgmanpage',
898 '--halt',
898 '--halt',
899 'warning',
899 'warning',
900 '--strip-elements-with-class',
900 '--strip-elements-with-class',
901 'htmlonly',
901 'htmlonly',
902 '%s.txt' % root,
902 '%s.txt' % root,
903 root,
903 root,
904 ],
904 ],
905 os.environ,
905 os.environ,
906 cwd='doc',
906 cwd='doc',
907 )
907 )
908 if res:
908 if res:
909 raise SystemExit(
909 raise SystemExit(
910 'error running runrst: %s' % '\n'.join([out, err])
910 'error running runrst: %s' % '\n'.join([out, err])
911 )
911 )
912
912
913 normalizecrlf('doc/%s' % root)
913 normalizecrlf('doc/%s' % root)
914
914
915 def genhtml(root):
915 def genhtml(root):
916 log.info('generating doc/%s.html' % root)
916 log.info('generating doc/%s.html' % root)
917 res, out, err = runcmd(
917 res, out, err = runcmd(
918 [
918 [
919 sys.executable,
919 sys.executable,
920 'runrst',
920 'runrst',
921 'html',
921 'html',
922 '--halt',
922 '--halt',
923 'warning',
923 'warning',
924 '--link-stylesheet',
924 '--link-stylesheet',
925 '--stylesheet-path',
925 '--stylesheet-path',
926 'style.css',
926 'style.css',
927 '%s.txt' % root,
927 '%s.txt' % root,
928 '%s.html' % root,
928 '%s.html' % root,
929 ],
929 ],
930 os.environ,
930 os.environ,
931 cwd='doc',
931 cwd='doc',
932 )
932 )
933 if res:
933 if res:
934 raise SystemExit(
934 raise SystemExit(
935 'error running runrst: %s' % '\n'.join([out, err])
935 'error running runrst: %s' % '\n'.join([out, err])
936 )
936 )
937
937
938 normalizecrlf('doc/%s.html' % root)
938 normalizecrlf('doc/%s.html' % root)
939
939
940 # This logic is duplicated in doc/Makefile.
940 # This logic is duplicated in doc/Makefile.
941 sources = {
941 sources = {
942 f
942 f
943 for f in os.listdir('mercurial/helptext')
943 for f in os.listdir('mercurial/helptext')
944 if re.search(r'[0-9]\.txt$', f)
944 if re.search(r'[0-9]\.txt$', f)
945 }
945 }
946
946
947 # common.txt is a one-off.
947 # common.txt is a one-off.
948 gentxt('common')
948 gentxt('common')
949
949
950 for source in sorted(sources):
950 for source in sorted(sources):
951 assert source[-4:] == '.txt'
951 assert source[-4:] == '.txt'
952 root = source[:-4]
952 root = source[:-4]
953
953
954 gentxt(root)
954 gentxt(root)
955 gengendoc(root)
955 gengendoc(root)
956
956
957 if self.man:
957 if self.man:
958 genman(root)
958 genman(root)
959 if self.html:
959 if self.html:
960 genhtml(root)
960 genhtml(root)
961
961
962
962
963 class hginstall(install):
963 class hginstall(install):
964
964
965 user_options = install.user_options + [
965 user_options = install.user_options + [
966 (
966 (
967 'old-and-unmanageable',
967 'old-and-unmanageable',
968 None,
968 None,
969 'noop, present for eggless setuptools compat',
969 'noop, present for eggless setuptools compat',
970 ),
970 ),
971 (
971 (
972 'single-version-externally-managed',
972 'single-version-externally-managed',
973 None,
973 None,
974 'noop, present for eggless setuptools compat',
974 'noop, present for eggless setuptools compat',
975 ),
975 ),
976 ]
976 ]
977
977
978 # Also helps setuptools not be sad while we refuse to create eggs.
978 # Also helps setuptools not be sad while we refuse to create eggs.
979 single_version_externally_managed = True
979 single_version_externally_managed = True
980
980
981 def get_sub_commands(self):
981 def get_sub_commands(self):
982 # Screen out egg related commands to prevent egg generation. But allow
982 # Screen out egg related commands to prevent egg generation. But allow
983 # mercurial.egg-info generation, since that is part of modern
983 # mercurial.egg-info generation, since that is part of modern
984 # packaging.
984 # packaging.
985 excl = {'bdist_egg'}
985 excl = {'bdist_egg'}
986 return filter(lambda x: x not in excl, install.get_sub_commands(self))
986 return filter(lambda x: x not in excl, install.get_sub_commands(self))
987
987
988
988
989 class hginstalllib(install_lib):
989 class hginstalllib(install_lib):
990 '''
990 '''
991 This is a specialization of install_lib that replaces the copy_file used
991 This is a specialization of install_lib that replaces the copy_file used
992 there so that it supports setting the mode of files after copying them,
992 there so that it supports setting the mode of files after copying them,
993 instead of just preserving the mode that the files originally had. If your
993 instead of just preserving the mode that the files originally had. If your
994 system has a umask of something like 027, preserving the permissions when
994 system has a umask of something like 027, preserving the permissions when
995 copying will lead to a broken install.
995 copying will lead to a broken install.
996
996
997 Note that just passing keep_permissions=False to copy_file would be
997 Note that just passing keep_permissions=False to copy_file would be
998 insufficient, as it might still be applying a umask.
998 insufficient, as it might still be applying a umask.
999 '''
999 '''
1000
1000
1001 def run(self):
1001 def run(self):
1002 realcopyfile = file_util.copy_file
1002 realcopyfile = file_util.copy_file
1003
1003
1004 def copyfileandsetmode(*args, **kwargs):
1004 def copyfileandsetmode(*args, **kwargs):
1005 src, dst = args[0], args[1]
1005 src, dst = args[0], args[1]
1006 dst, copied = realcopyfile(*args, **kwargs)
1006 dst, copied = realcopyfile(*args, **kwargs)
1007 if copied:
1007 if copied:
1008 st = os.stat(src)
1008 st = os.stat(src)
1009 # Persist executable bit (apply it to group and other if user
1009 # Persist executable bit (apply it to group and other if user
1010 # has it)
1010 # has it)
1011 if st[stat.ST_MODE] & stat.S_IXUSR:
1011 if st[stat.ST_MODE] & stat.S_IXUSR:
1012 setmode = int('0755', 8)
1012 setmode = int('0755', 8)
1013 else:
1013 else:
1014 setmode = int('0644', 8)
1014 setmode = int('0644', 8)
1015 m = stat.S_IMODE(st[stat.ST_MODE])
1015 m = stat.S_IMODE(st[stat.ST_MODE])
1016 m = (m & ~int('0777', 8)) | setmode
1016 m = (m & ~int('0777', 8)) | setmode
1017 os.chmod(dst, m)
1017 os.chmod(dst, m)
1018
1018
1019 file_util.copy_file = copyfileandsetmode
1019 file_util.copy_file = copyfileandsetmode
1020 try:
1020 try:
1021 install_lib.run(self)
1021 install_lib.run(self)
1022 finally:
1022 finally:
1023 file_util.copy_file = realcopyfile
1023 file_util.copy_file = realcopyfile
1024
1024
1025
1025
1026 class hginstallscripts(install_scripts):
1026 class hginstallscripts(install_scripts):
1027 '''
1027 '''
1028 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1028 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1029 the configured directory for modules. If possible, the path is made relative
1029 the configured directory for modules. If possible, the path is made relative
1030 to the directory for scripts.
1030 to the directory for scripts.
1031 '''
1031 '''
1032
1032
1033 def initialize_options(self):
1033 def initialize_options(self):
1034 install_scripts.initialize_options(self)
1034 install_scripts.initialize_options(self)
1035
1035
1036 self.install_lib = None
1036 self.install_lib = None
1037
1037
1038 def finalize_options(self):
1038 def finalize_options(self):
1039 install_scripts.finalize_options(self)
1039 install_scripts.finalize_options(self)
1040 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1040 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1041
1041
1042 def run(self):
1042 def run(self):
1043 install_scripts.run(self)
1043 install_scripts.run(self)
1044
1044
1045 # It only makes sense to replace @LIBDIR@ with the install path if
1045 # It only makes sense to replace @LIBDIR@ with the install path if
1046 # the install path is known. For wheels, the logic below calculates
1046 # the install path is known. For wheels, the logic below calculates
1047 # the libdir to be "../..". This is because the internal layout of a
1047 # the libdir to be "../..". This is because the internal layout of a
1048 # wheel archive looks like:
1048 # wheel archive looks like:
1049 #
1049 #
1050 # mercurial-3.6.1.data/scripts/hg
1050 # mercurial-3.6.1.data/scripts/hg
1051 # mercurial/__init__.py
1051 # mercurial/__init__.py
1052 #
1052 #
1053 # When installing wheels, the subdirectories of the "<pkg>.data"
1053 # When installing wheels, the subdirectories of the "<pkg>.data"
1054 # directory are translated to system local paths and files therein
1054 # directory are translated to system local paths and files therein
1055 # are copied in place. The mercurial/* files are installed into the
1055 # are copied in place. The mercurial/* files are installed into the
1056 # site-packages directory. However, the site-packages directory
1056 # site-packages directory. However, the site-packages directory
1057 # isn't known until wheel install time. This means we have no clue
1057 # isn't known until wheel install time. This means we have no clue
1058 # at wheel generation time what the installed site-packages directory
1058 # at wheel generation time what the installed site-packages directory
1059 # will be. And, wheels don't appear to provide the ability to register
1059 # will be. And, wheels don't appear to provide the ability to register
1060 # custom code to run during wheel installation. This all means that
1060 # custom code to run during wheel installation. This all means that
1061 # we can't reliably set the libdir in wheels: the default behavior
1061 # we can't reliably set the libdir in wheels: the default behavior
1062 # of looking in sys.path must do.
1062 # of looking in sys.path must do.
1063
1063
1064 if (
1064 if (
1065 os.path.splitdrive(self.install_dir)[0]
1065 os.path.splitdrive(self.install_dir)[0]
1066 != os.path.splitdrive(self.install_lib)[0]
1066 != os.path.splitdrive(self.install_lib)[0]
1067 ):
1067 ):
1068 # can't make relative paths from one drive to another, so use an
1068 # can't make relative paths from one drive to another, so use an
1069 # absolute path instead
1069 # absolute path instead
1070 libdir = self.install_lib
1070 libdir = self.install_lib
1071 else:
1071 else:
1072 libdir = os.path.relpath(self.install_lib, self.install_dir)
1072 libdir = os.path.relpath(self.install_lib, self.install_dir)
1073
1073
1074 for outfile in self.outfiles:
1074 for outfile in self.outfiles:
1075 with open(outfile, 'rb') as fp:
1075 with open(outfile, 'rb') as fp:
1076 data = fp.read()
1076 data = fp.read()
1077
1077
1078 # skip binary files
1078 # skip binary files
1079 if b'\0' in data:
1079 if b'\0' in data:
1080 continue
1080 continue
1081
1081
1082 # During local installs, the shebang will be rewritten to the final
1082 # During local installs, the shebang will be rewritten to the final
1083 # install path. During wheel packaging, the shebang has a special
1083 # install path. During wheel packaging, the shebang has a special
1084 # value.
1084 # value.
1085 if data.startswith(b'#!python'):
1085 if data.startswith(b'#!python'):
1086 log.info(
1086 log.info(
1087 'not rewriting @LIBDIR@ in %s because install path '
1087 'not rewriting @LIBDIR@ in %s because install path '
1088 'not known' % outfile
1088 'not known' % outfile
1089 )
1089 )
1090 continue
1090 continue
1091
1091
1092 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
1092 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
1093 with open(outfile, 'wb') as fp:
1093 with open(outfile, 'wb') as fp:
1094 fp.write(data)
1094 fp.write(data)
1095
1095
1096
1096
1097 # virtualenv installs custom distutils/__init__.py and
1097 # virtualenv installs custom distutils/__init__.py and
1098 # distutils/distutils.cfg files which essentially proxy back to the
1098 # distutils/distutils.cfg files which essentially proxy back to the
1099 # "real" distutils in the main Python install. The presence of this
1099 # "real" distutils in the main Python install. The presence of this
1100 # directory causes py2exe to pick up the "hacked" distutils package
1100 # directory causes py2exe to pick up the "hacked" distutils package
1101 # from the virtualenv and "import distutils" will fail from the py2exe
1101 # from the virtualenv and "import distutils" will fail from the py2exe
1102 # build because the "real" distutils files can't be located.
1102 # build because the "real" distutils files can't be located.
1103 #
1103 #
1104 # We work around this by monkeypatching the py2exe code finding Python
1104 # We work around this by monkeypatching the py2exe code finding Python
1105 # modules to replace the found virtualenv distutils modules with the
1105 # modules to replace the found virtualenv distutils modules with the
1106 # original versions via filesystem scanning. This is a bit hacky. But
1106 # original versions via filesystem scanning. This is a bit hacky. But
1107 # it allows us to use virtualenvs for py2exe packaging, which is more
1107 # it allows us to use virtualenvs for py2exe packaging, which is more
1108 # deterministic and reproducible.
1108 # deterministic and reproducible.
1109 #
1109 #
1110 # It's worth noting that the common StackOverflow suggestions for this
1110 # It's worth noting that the common StackOverflow suggestions for this
1111 # problem involve copying the original distutils files into the
1111 # problem involve copying the original distutils files into the
1112 # virtualenv or into the staging directory after setup() is invoked.
1112 # virtualenv or into the staging directory after setup() is invoked.
1113 # The former is very brittle and can easily break setup(). Our hacking
1113 # The former is very brittle and can easily break setup(). Our hacking
1114 # of the found modules routine has a similar result as copying the files
1114 # of the found modules routine has a similar result as copying the files
1115 # manually. But it makes fewer assumptions about how py2exe works and
1115 # manually. But it makes fewer assumptions about how py2exe works and
1116 # is less brittle.
1116 # is less brittle.
1117
1117
1118 # This only catches virtualenvs made with virtualenv (as opposed to
1118 # This only catches virtualenvs made with virtualenv (as opposed to
1119 # venv, which is likely what Python 3 uses).
1119 # venv, which is likely what Python 3 uses).
1120 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1120 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1121
1121
1122 if py2exehacked:
1122 if py2exehacked:
1123 from distutils.command.py2exe import py2exe as buildpy2exe
1123 from distutils.command.py2exe import py2exe as buildpy2exe
1124 from py2exe.mf import Module as py2exemodule
1124 from py2exe.mf import Module as py2exemodule
1125
1125
1126 class hgbuildpy2exe(buildpy2exe):
1126 class hgbuildpy2exe(buildpy2exe):
1127 def find_needed_modules(self, mf, files, modules):
1127 def find_needed_modules(self, mf, files, modules):
1128 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1128 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1129
1129
1130 # Replace virtualenv's distutils modules with the real ones.
1130 # Replace virtualenv's distutils modules with the real ones.
1131 modules = {}
1131 modules = {}
1132 for k, v in res.modules.items():
1132 for k, v in res.modules.items():
1133 if k != 'distutils' and not k.startswith('distutils.'):
1133 if k != 'distutils' and not k.startswith('distutils.'):
1134 modules[k] = v
1134 modules[k] = v
1135
1135
1136 res.modules = modules
1136 res.modules = modules
1137
1137
1138 import opcode
1138 import opcode
1139
1139
1140 distutilsreal = os.path.join(
1140 distutilsreal = os.path.join(
1141 os.path.dirname(opcode.__file__), 'distutils'
1141 os.path.dirname(opcode.__file__), 'distutils'
1142 )
1142 )
1143
1143
1144 for root, dirs, files in os.walk(distutilsreal):
1144 for root, dirs, files in os.walk(distutilsreal):
1145 for f in sorted(files):
1145 for f in sorted(files):
1146 if not f.endswith('.py'):
1146 if not f.endswith('.py'):
1147 continue
1147 continue
1148
1148
1149 full = os.path.join(root, f)
1149 full = os.path.join(root, f)
1150
1150
1151 parents = ['distutils']
1151 parents = ['distutils']
1152
1152
1153 if root != distutilsreal:
1153 if root != distutilsreal:
1154 rel = os.path.relpath(root, distutilsreal)
1154 rel = os.path.relpath(root, distutilsreal)
1155 parents.extend(p for p in rel.split(os.sep))
1155 parents.extend(p for p in rel.split(os.sep))
1156
1156
1157 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1157 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1158
1158
1159 if modname.startswith('distutils.tests.'):
1159 if modname.startswith('distutils.tests.'):
1160 continue
1160 continue
1161
1161
1162 if modname.endswith('.__init__'):
1162 if modname.endswith('.__init__'):
1163 modname = modname[: -len('.__init__')]
1163 modname = modname[: -len('.__init__')]
1164 path = os.path.dirname(full)
1164 path = os.path.dirname(full)
1165 else:
1165 else:
1166 path = None
1166 path = None
1167
1167
1168 res.modules[modname] = py2exemodule(
1168 res.modules[modname] = py2exemodule(
1169 modname, full, path=path
1169 modname, full, path=path
1170 )
1170 )
1171
1171
1172 if 'distutils' not in res.modules:
1172 if 'distutils' not in res.modules:
1173 raise SystemExit('could not find distutils modules')
1173 raise SystemExit('could not find distutils modules')
1174
1174
1175 return res
1175 return res
1176
1176
1177
1177
1178 cmdclass = {
1178 cmdclass = {
1179 'build': hgbuild,
1179 'build': hgbuild,
1180 'build_doc': hgbuilddoc,
1180 'build_doc': hgbuilddoc,
1181 'build_mo': hgbuildmo,
1181 'build_mo': hgbuildmo,
1182 'build_ext': hgbuildext,
1182 'build_ext': hgbuildext,
1183 'build_py': hgbuildpy,
1183 'build_py': hgbuildpy,
1184 'build_scripts': hgbuildscripts,
1184 'build_scripts': hgbuildscripts,
1185 'build_hgextindex': buildhgextindex,
1185 'build_hgextindex': buildhgextindex,
1186 'install': hginstall,
1186 'install': hginstall,
1187 'install_lib': hginstalllib,
1187 'install_lib': hginstalllib,
1188 'install_scripts': hginstallscripts,
1188 'install_scripts': hginstallscripts,
1189 'build_hgexe': buildhgexe,
1189 'build_hgexe': buildhgexe,
1190 }
1190 }
1191
1191
1192 if py2exehacked:
1192 if py2exehacked:
1193 cmdclass['py2exe'] = hgbuildpy2exe
1193 cmdclass['py2exe'] = hgbuildpy2exe
1194
1194
1195 packages = [
1195 packages = [
1196 'mercurial',
1196 'mercurial',
1197 'mercurial.cext',
1197 'mercurial.cext',
1198 'mercurial.cffi',
1198 'mercurial.cffi',
1199 'mercurial.defaultrc',
1199 'mercurial.defaultrc',
1200 'mercurial.helptext',
1200 'mercurial.helptext',
1201 'mercurial.helptext.internals',
1201 'mercurial.helptext.internals',
1202 'mercurial.hgweb',
1202 'mercurial.hgweb',
1203 'mercurial.interfaces',
1203 'mercurial.interfaces',
1204 'mercurial.pure',
1204 'mercurial.pure',
1205 'mercurial.thirdparty',
1205 'mercurial.thirdparty',
1206 'mercurial.thirdparty.attr',
1206 'mercurial.thirdparty.attr',
1207 'mercurial.thirdparty.zope',
1207 'mercurial.thirdparty.zope',
1208 'mercurial.thirdparty.zope.interface',
1208 'mercurial.thirdparty.zope.interface',
1209 'mercurial.utils',
1209 'mercurial.utils',
1210 'mercurial.revlogutils',
1210 'mercurial.revlogutils',
1211 'mercurial.testing',
1211 'mercurial.testing',
1212 'hgext',
1212 'hgext',
1213 'hgext.convert',
1213 'hgext.convert',
1214 'hgext.fsmonitor',
1214 'hgext.fsmonitor',
1215 'hgext.fastannotate',
1215 'hgext.fastannotate',
1216 'hgext.fsmonitor.pywatchman',
1216 'hgext.fsmonitor.pywatchman',
1217 'hgext.git',
1217 'hgext.highlight',
1218 'hgext.highlight',
1218 'hgext.hooklib',
1219 'hgext.hooklib',
1219 'hgext.infinitepush',
1220 'hgext.infinitepush',
1220 'hgext.largefiles',
1221 'hgext.largefiles',
1221 'hgext.lfs',
1222 'hgext.lfs',
1222 'hgext.narrow',
1223 'hgext.narrow',
1223 'hgext.remotefilelog',
1224 'hgext.remotefilelog',
1224 'hgext.zeroconf',
1225 'hgext.zeroconf',
1225 'hgext3rd',
1226 'hgext3rd',
1226 'hgdemandimport',
1227 'hgdemandimport',
1227 ]
1228 ]
1228 if sys.version_info[0] == 2:
1229 if sys.version_info[0] == 2:
1229 packages.extend(
1230 packages.extend(
1230 [
1231 [
1231 'mercurial.thirdparty.concurrent',
1232 'mercurial.thirdparty.concurrent',
1232 'mercurial.thirdparty.concurrent.futures',
1233 'mercurial.thirdparty.concurrent.futures',
1233 ]
1234 ]
1234 )
1235 )
1235
1236
1236 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1237 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1237 # py2exe can't cope with namespace packages very well, so we have to
1238 # py2exe can't cope with namespace packages very well, so we have to
1238 # install any hgext3rd.* extensions that we want in the final py2exe
1239 # install any hgext3rd.* extensions that we want in the final py2exe
1239 # image here. This is gross, but you gotta do what you gotta do.
1240 # image here. This is gross, but you gotta do what you gotta do.
1240 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1241 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1241
1242
1242 common_depends = [
1243 common_depends = [
1243 'mercurial/bitmanipulation.h',
1244 'mercurial/bitmanipulation.h',
1244 'mercurial/compat.h',
1245 'mercurial/compat.h',
1245 'mercurial/cext/util.h',
1246 'mercurial/cext/util.h',
1246 ]
1247 ]
1247 common_include_dirs = ['mercurial']
1248 common_include_dirs = ['mercurial']
1248
1249
1249 osutil_cflags = []
1250 osutil_cflags = []
1250 osutil_ldflags = []
1251 osutil_ldflags = []
1251
1252
1252 # platform specific macros
1253 # platform specific macros
1253 for plat, func in [('bsd', 'setproctitle')]:
1254 for plat, func in [('bsd', 'setproctitle')]:
1254 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1255 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1255 osutil_cflags.append('-DHAVE_%s' % func.upper())
1256 osutil_cflags.append('-DHAVE_%s' % func.upper())
1256
1257
1257 for plat, macro, code in [
1258 for plat, macro, code in [
1258 (
1259 (
1259 'bsd|darwin',
1260 'bsd|darwin',
1260 'BSD_STATFS',
1261 'BSD_STATFS',
1261 '''
1262 '''
1262 #include <sys/param.h>
1263 #include <sys/param.h>
1263 #include <sys/mount.h>
1264 #include <sys/mount.h>
1264 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1265 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1265 ''',
1266 ''',
1266 ),
1267 ),
1267 (
1268 (
1268 'linux',
1269 'linux',
1269 'LINUX_STATFS',
1270 'LINUX_STATFS',
1270 '''
1271 '''
1271 #include <linux/magic.h>
1272 #include <linux/magic.h>
1272 #include <sys/vfs.h>
1273 #include <sys/vfs.h>
1273 int main() { struct statfs s; return sizeof(s.f_type); }
1274 int main() { struct statfs s; return sizeof(s.f_type); }
1274 ''',
1275 ''',
1275 ),
1276 ),
1276 ]:
1277 ]:
1277 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1278 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1278 osutil_cflags.append('-DHAVE_%s' % macro)
1279 osutil_cflags.append('-DHAVE_%s' % macro)
1279
1280
1280 if sys.platform == 'darwin':
1281 if sys.platform == 'darwin':
1281 osutil_ldflags += ['-framework', 'ApplicationServices']
1282 osutil_ldflags += ['-framework', 'ApplicationServices']
1282
1283
1283 xdiff_srcs = [
1284 xdiff_srcs = [
1284 'mercurial/thirdparty/xdiff/xdiffi.c',
1285 'mercurial/thirdparty/xdiff/xdiffi.c',
1285 'mercurial/thirdparty/xdiff/xprepare.c',
1286 'mercurial/thirdparty/xdiff/xprepare.c',
1286 'mercurial/thirdparty/xdiff/xutils.c',
1287 'mercurial/thirdparty/xdiff/xutils.c',
1287 ]
1288 ]
1288
1289
1289 xdiff_headers = [
1290 xdiff_headers = [
1290 'mercurial/thirdparty/xdiff/xdiff.h',
1291 'mercurial/thirdparty/xdiff/xdiff.h',
1291 'mercurial/thirdparty/xdiff/xdiffi.h',
1292 'mercurial/thirdparty/xdiff/xdiffi.h',
1292 'mercurial/thirdparty/xdiff/xinclude.h',
1293 'mercurial/thirdparty/xdiff/xinclude.h',
1293 'mercurial/thirdparty/xdiff/xmacros.h',
1294 'mercurial/thirdparty/xdiff/xmacros.h',
1294 'mercurial/thirdparty/xdiff/xprepare.h',
1295 'mercurial/thirdparty/xdiff/xprepare.h',
1295 'mercurial/thirdparty/xdiff/xtypes.h',
1296 'mercurial/thirdparty/xdiff/xtypes.h',
1296 'mercurial/thirdparty/xdiff/xutils.h',
1297 'mercurial/thirdparty/xdiff/xutils.h',
1297 ]
1298 ]
1298
1299
1299
1300
1300 class RustCompilationError(CCompilerError):
1301 class RustCompilationError(CCompilerError):
1301 """Exception class for Rust compilation errors."""
1302 """Exception class for Rust compilation errors."""
1302
1303
1303
1304
1304 class RustExtension(Extension):
1305 class RustExtension(Extension):
1305 """Base classes for concrete Rust Extension classes.
1306 """Base classes for concrete Rust Extension classes.
1306 """
1307 """
1307
1308
1308 rusttargetdir = os.path.join('rust', 'target', 'release')
1309 rusttargetdir = os.path.join('rust', 'target', 'release')
1309
1310
1310 def __init__(
1311 def __init__(
1311 self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw
1312 self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw
1312 ):
1313 ):
1313 Extension.__init__(self, mpath, sources, **kw)
1314 Extension.__init__(self, mpath, sources, **kw)
1314 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1315 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1315 self.py3_features = py3_features
1316 self.py3_features = py3_features
1316
1317
1317 # adding Rust source and control files to depends so that the extension
1318 # adding Rust source and control files to depends so that the extension
1318 # gets rebuilt if they've changed
1319 # gets rebuilt if they've changed
1319 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1320 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1320 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1321 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1321 if os.path.exists(cargo_lock):
1322 if os.path.exists(cargo_lock):
1322 self.depends.append(cargo_lock)
1323 self.depends.append(cargo_lock)
1323 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1324 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1324 self.depends.extend(
1325 self.depends.extend(
1325 os.path.join(dirpath, fname)
1326 os.path.join(dirpath, fname)
1326 for fname in fnames
1327 for fname in fnames
1327 if os.path.splitext(fname)[1] == '.rs'
1328 if os.path.splitext(fname)[1] == '.rs'
1328 )
1329 )
1329
1330
1330 @staticmethod
1331 @staticmethod
1331 def rustdylibsuffix():
1332 def rustdylibsuffix():
1332 """Return the suffix for shared libraries produced by rustc.
1333 """Return the suffix for shared libraries produced by rustc.
1333
1334
1334 See also: https://doc.rust-lang.org/reference/linkage.html
1335 See also: https://doc.rust-lang.org/reference/linkage.html
1335 """
1336 """
1336 if sys.platform == 'darwin':
1337 if sys.platform == 'darwin':
1337 return '.dylib'
1338 return '.dylib'
1338 elif os.name == 'nt':
1339 elif os.name == 'nt':
1339 return '.dll'
1340 return '.dll'
1340 else:
1341 else:
1341 return '.so'
1342 return '.so'
1342
1343
1343 def rustbuild(self):
1344 def rustbuild(self):
1344 env = os.environ.copy()
1345 env = os.environ.copy()
1345 if 'HGTEST_RESTOREENV' in env:
1346 if 'HGTEST_RESTOREENV' in env:
1346 # Mercurial tests change HOME to a temporary directory,
1347 # Mercurial tests change HOME to a temporary directory,
1347 # but, if installed with rustup, the Rust toolchain needs
1348 # but, if installed with rustup, the Rust toolchain needs
1348 # HOME to be correct (otherwise the 'no default toolchain'
1349 # HOME to be correct (otherwise the 'no default toolchain'
1349 # error message is issued and the build fails).
1350 # error message is issued and the build fails).
1350 # This happens currently with test-hghave.t, which does
1351 # This happens currently with test-hghave.t, which does
1351 # invoke this build.
1352 # invoke this build.
1352
1353
1353 # Unix only fix (os.path.expanduser not really reliable if
1354 # Unix only fix (os.path.expanduser not really reliable if
1354 # HOME is shadowed like this)
1355 # HOME is shadowed like this)
1355 import pwd
1356 import pwd
1356
1357
1357 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1358 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1358
1359
1359 cargocmd = ['cargo', 'rustc', '-vv', '--release']
1360 cargocmd = ['cargo', 'rustc', '-vv', '--release']
1360
1361
1361 feature_flags = []
1362 feature_flags = []
1362
1363
1363 if sys.version_info[0] == 3 and self.py3_features is not None:
1364 if sys.version_info[0] == 3 and self.py3_features is not None:
1364 feature_flags.append(self.py3_features)
1365 feature_flags.append(self.py3_features)
1365 cargocmd.append('--no-default-features')
1366 cargocmd.append('--no-default-features')
1366
1367
1367 rust_features = env.get("HG_RUST_FEATURES")
1368 rust_features = env.get("HG_RUST_FEATURES")
1368 if rust_features:
1369 if rust_features:
1369 feature_flags.append(rust_features)
1370 feature_flags.append(rust_features)
1370
1371
1371 cargocmd.extend(('--features', " ".join(feature_flags)))
1372 cargocmd.extend(('--features', " ".join(feature_flags)))
1372
1373
1373 cargocmd.append('--')
1374 cargocmd.append('--')
1374 if sys.platform == 'darwin':
1375 if sys.platform == 'darwin':
1375 cargocmd.extend(
1376 cargocmd.extend(
1376 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1377 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1377 )
1378 )
1378 try:
1379 try:
1379 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1380 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1380 except OSError as exc:
1381 except OSError as exc:
1381 if exc.errno == errno.ENOENT:
1382 if exc.errno == errno.ENOENT:
1382 raise RustCompilationError("Cargo not found")
1383 raise RustCompilationError("Cargo not found")
1383 elif exc.errno == errno.EACCES:
1384 elif exc.errno == errno.EACCES:
1384 raise RustCompilationError(
1385 raise RustCompilationError(
1385 "Cargo found, but permisssion to execute it is denied"
1386 "Cargo found, but permisssion to execute it is denied"
1386 )
1387 )
1387 else:
1388 else:
1388 raise
1389 raise
1389 except subprocess.CalledProcessError:
1390 except subprocess.CalledProcessError:
1390 raise RustCompilationError(
1391 raise RustCompilationError(
1391 "Cargo failed. Working directory: %r, "
1392 "Cargo failed. Working directory: %r, "
1392 "command: %r, environment: %r"
1393 "command: %r, environment: %r"
1393 % (self.rustsrcdir, cargocmd, env)
1394 % (self.rustsrcdir, cargocmd, env)
1394 )
1395 )
1395
1396
1396
1397
1397 class RustStandaloneExtension(RustExtension):
1398 class RustStandaloneExtension(RustExtension):
1398 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1399 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1399 RustExtension.__init__(
1400 RustExtension.__init__(
1400 self, pydottedname, [], dylibname, rustcrate, **kw
1401 self, pydottedname, [], dylibname, rustcrate, **kw
1401 )
1402 )
1402 self.dylibname = dylibname
1403 self.dylibname = dylibname
1403
1404
1404 def build(self, target_dir):
1405 def build(self, target_dir):
1405 self.rustbuild()
1406 self.rustbuild()
1406 target = [target_dir]
1407 target = [target_dir]
1407 target.extend(self.name.split('.'))
1408 target.extend(self.name.split('.'))
1408 target[-1] += DYLIB_SUFFIX
1409 target[-1] += DYLIB_SUFFIX
1409 shutil.copy2(
1410 shutil.copy2(
1410 os.path.join(
1411 os.path.join(
1411 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1412 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1412 ),
1413 ),
1413 os.path.join(*target),
1414 os.path.join(*target),
1414 )
1415 )
1415
1416
1416
1417
1417 extmodules = [
1418 extmodules = [
1418 Extension(
1419 Extension(
1419 'mercurial.cext.base85',
1420 'mercurial.cext.base85',
1420 ['mercurial/cext/base85.c'],
1421 ['mercurial/cext/base85.c'],
1421 include_dirs=common_include_dirs,
1422 include_dirs=common_include_dirs,
1422 depends=common_depends,
1423 depends=common_depends,
1423 ),
1424 ),
1424 Extension(
1425 Extension(
1425 'mercurial.cext.bdiff',
1426 'mercurial.cext.bdiff',
1426 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1427 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1427 include_dirs=common_include_dirs,
1428 include_dirs=common_include_dirs,
1428 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1429 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1429 ),
1430 ),
1430 Extension(
1431 Extension(
1431 'mercurial.cext.mpatch',
1432 'mercurial.cext.mpatch',
1432 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1433 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1433 include_dirs=common_include_dirs,
1434 include_dirs=common_include_dirs,
1434 depends=common_depends,
1435 depends=common_depends,
1435 ),
1436 ),
1436 Extension(
1437 Extension(
1437 'mercurial.cext.parsers',
1438 'mercurial.cext.parsers',
1438 [
1439 [
1439 'mercurial/cext/charencode.c',
1440 'mercurial/cext/charencode.c',
1440 'mercurial/cext/dirs.c',
1441 'mercurial/cext/dirs.c',
1441 'mercurial/cext/manifest.c',
1442 'mercurial/cext/manifest.c',
1442 'mercurial/cext/parsers.c',
1443 'mercurial/cext/parsers.c',
1443 'mercurial/cext/pathencode.c',
1444 'mercurial/cext/pathencode.c',
1444 'mercurial/cext/revlog.c',
1445 'mercurial/cext/revlog.c',
1445 ],
1446 ],
1446 include_dirs=common_include_dirs,
1447 include_dirs=common_include_dirs,
1447 depends=common_depends
1448 depends=common_depends
1448 + ['mercurial/cext/charencode.h', 'mercurial/cext/revlog.h',],
1449 + ['mercurial/cext/charencode.h', 'mercurial/cext/revlog.h',],
1449 ),
1450 ),
1450 Extension(
1451 Extension(
1451 'mercurial.cext.osutil',
1452 'mercurial.cext.osutil',
1452 ['mercurial/cext/osutil.c'],
1453 ['mercurial/cext/osutil.c'],
1453 include_dirs=common_include_dirs,
1454 include_dirs=common_include_dirs,
1454 extra_compile_args=osutil_cflags,
1455 extra_compile_args=osutil_cflags,
1455 extra_link_args=osutil_ldflags,
1456 extra_link_args=osutil_ldflags,
1456 depends=common_depends,
1457 depends=common_depends,
1457 ),
1458 ),
1458 Extension(
1459 Extension(
1459 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1460 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1460 [
1461 [
1461 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1462 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1462 ],
1463 ],
1463 ),
1464 ),
1464 Extension(
1465 Extension(
1465 'mercurial.thirdparty.sha1dc',
1466 'mercurial.thirdparty.sha1dc',
1466 [
1467 [
1467 'mercurial/thirdparty/sha1dc/cext.c',
1468 'mercurial/thirdparty/sha1dc/cext.c',
1468 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1469 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1469 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1470 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1470 ],
1471 ],
1471 ),
1472 ),
1472 Extension(
1473 Extension(
1473 'hgext.fsmonitor.pywatchman.bser', ['hgext/fsmonitor/pywatchman/bser.c']
1474 'hgext.fsmonitor.pywatchman.bser', ['hgext/fsmonitor/pywatchman/bser.c']
1474 ),
1475 ),
1475 RustStandaloneExtension(
1476 RustStandaloneExtension(
1476 'mercurial.rustext', 'hg-cpython', 'librusthg', py3_features='python3'
1477 'mercurial.rustext', 'hg-cpython', 'librusthg', py3_features='python3'
1477 ),
1478 ),
1478 ]
1479 ]
1479
1480
1480
1481
1481 sys.path.insert(0, 'contrib/python-zstandard')
1482 sys.path.insert(0, 'contrib/python-zstandard')
1482 import setup_zstd
1483 import setup_zstd
1483
1484
1484 extmodules.append(
1485 extmodules.append(
1485 setup_zstd.get_c_extension(
1486 setup_zstd.get_c_extension(
1486 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1487 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1487 )
1488 )
1488 )
1489 )
1489
1490
1490 try:
1491 try:
1491 from distutils import cygwinccompiler
1492 from distutils import cygwinccompiler
1492
1493
1493 # the -mno-cygwin option has been deprecated for years
1494 # the -mno-cygwin option has been deprecated for years
1494 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1495 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1495
1496
1496 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1497 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1497 def __init__(self, *args, **kwargs):
1498 def __init__(self, *args, **kwargs):
1498 mingw32compilerclass.__init__(self, *args, **kwargs)
1499 mingw32compilerclass.__init__(self, *args, **kwargs)
1499 for i in 'compiler compiler_so linker_exe linker_so'.split():
1500 for i in 'compiler compiler_so linker_exe linker_so'.split():
1500 try:
1501 try:
1501 getattr(self, i).remove('-mno-cygwin')
1502 getattr(self, i).remove('-mno-cygwin')
1502 except ValueError:
1503 except ValueError:
1503 pass
1504 pass
1504
1505
1505 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1506 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1506 except ImportError:
1507 except ImportError:
1507 # the cygwinccompiler package is not available on some Python
1508 # the cygwinccompiler package is not available on some Python
1508 # distributions like the ones from the optware project for Synology
1509 # distributions like the ones from the optware project for Synology
1509 # DiskStation boxes
1510 # DiskStation boxes
1510 class HackedMingw32CCompiler(object):
1511 class HackedMingw32CCompiler(object):
1511 pass
1512 pass
1512
1513
1513
1514
1514 if os.name == 'nt':
1515 if os.name == 'nt':
1515 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1516 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1516 # extra_link_args to distutils.extensions.Extension() doesn't have any
1517 # extra_link_args to distutils.extensions.Extension() doesn't have any
1517 # effect.
1518 # effect.
1518 from distutils import msvccompiler
1519 from distutils import msvccompiler
1519
1520
1520 msvccompilerclass = msvccompiler.MSVCCompiler
1521 msvccompilerclass = msvccompiler.MSVCCompiler
1521
1522
1522 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1523 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1523 def initialize(self):
1524 def initialize(self):
1524 msvccompilerclass.initialize(self)
1525 msvccompilerclass.initialize(self)
1525 # "warning LNK4197: export 'func' specified multiple times"
1526 # "warning LNK4197: export 'func' specified multiple times"
1526 self.ldflags_shared.append('/ignore:4197')
1527 self.ldflags_shared.append('/ignore:4197')
1527 self.ldflags_shared_debug.append('/ignore:4197')
1528 self.ldflags_shared_debug.append('/ignore:4197')
1528
1529
1529 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1530 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1530
1531
1531 packagedata = {
1532 packagedata = {
1532 'mercurial': [
1533 'mercurial': [
1533 'locale/*/LC_MESSAGES/hg.mo',
1534 'locale/*/LC_MESSAGES/hg.mo',
1534 'defaultrc/*.rc',
1535 'defaultrc/*.rc',
1535 'dummycert.pem',
1536 'dummycert.pem',
1536 ],
1537 ],
1537 'mercurial.helptext': ['*.txt',],
1538 'mercurial.helptext': ['*.txt',],
1538 'mercurial.helptext.internals': ['*.txt',],
1539 'mercurial.helptext.internals': ['*.txt',],
1539 }
1540 }
1540
1541
1541
1542
1542 def ordinarypath(p):
1543 def ordinarypath(p):
1543 return p and p[0] != '.' and p[-1] != '~'
1544 return p and p[0] != '.' and p[-1] != '~'
1544
1545
1545
1546
1546 for root in ('templates',):
1547 for root in ('templates',):
1547 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1548 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1548 curdir = curdir.split(os.sep, 1)[1]
1549 curdir = curdir.split(os.sep, 1)[1]
1549 dirs[:] = filter(ordinarypath, dirs)
1550 dirs[:] = filter(ordinarypath, dirs)
1550 for f in filter(ordinarypath, files):
1551 for f in filter(ordinarypath, files):
1551 f = os.path.join(curdir, f)
1552 f = os.path.join(curdir, f)
1552 packagedata['mercurial'].append(f)
1553 packagedata['mercurial'].append(f)
1553
1554
1554 datafiles = []
1555 datafiles = []
1555
1556
1556 # distutils expects version to be str/unicode. Converting it to
1557 # distutils expects version to be str/unicode. Converting it to
1557 # unicode on Python 2 still works because it won't contain any
1558 # unicode on Python 2 still works because it won't contain any
1558 # non-ascii bytes and will be implicitly converted back to bytes
1559 # non-ascii bytes and will be implicitly converted back to bytes
1559 # when operated on.
1560 # when operated on.
1560 assert isinstance(version, bytes)
1561 assert isinstance(version, bytes)
1561 setupversion = version.decode('ascii')
1562 setupversion = version.decode('ascii')
1562
1563
1563 extra = {}
1564 extra = {}
1564
1565
1565 py2exepackages = [
1566 py2exepackages = [
1566 'hgdemandimport',
1567 'hgdemandimport',
1567 'hgext3rd',
1568 'hgext3rd',
1568 'hgext',
1569 'hgext',
1569 'email',
1570 'email',
1570 # implicitly imported per module policy
1571 # implicitly imported per module policy
1571 # (cffi wouldn't be used as a frozen exe)
1572 # (cffi wouldn't be used as a frozen exe)
1572 'mercurial.cext',
1573 'mercurial.cext',
1573 #'mercurial.cffi',
1574 #'mercurial.cffi',
1574 'mercurial.pure',
1575 'mercurial.pure',
1575 ]
1576 ]
1576
1577
1577 py2exeexcludes = []
1578 py2exeexcludes = []
1578 py2exedllexcludes = ['crypt32.dll']
1579 py2exedllexcludes = ['crypt32.dll']
1579
1580
1580 if issetuptools:
1581 if issetuptools:
1581 extra['python_requires'] = supportedpy
1582 extra['python_requires'] = supportedpy
1582
1583
1583 if py2exeloaded:
1584 if py2exeloaded:
1584 extra['console'] = [
1585 extra['console'] = [
1585 {
1586 {
1586 'script': 'hg',
1587 'script': 'hg',
1587 'copyright': 'Copyright (C) 2005-2020 Matt Mackall and others',
1588 'copyright': 'Copyright (C) 2005-2020 Matt Mackall and others',
1588 'product_version': version,
1589 'product_version': version,
1589 }
1590 }
1590 ]
1591 ]
1591 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1592 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1592 # Need to override hgbuild because it has a private copy of
1593 # Need to override hgbuild because it has a private copy of
1593 # build.sub_commands.
1594 # build.sub_commands.
1594 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1595 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1595 # put dlls in sub directory so that they won't pollute PATH
1596 # put dlls in sub directory so that they won't pollute PATH
1596 extra['zipfile'] = 'lib/library.zip'
1597 extra['zipfile'] = 'lib/library.zip'
1597
1598
1598 # We allow some configuration to be supplemented via environment
1599 # We allow some configuration to be supplemented via environment
1599 # variables. This is better than setup.cfg files because it allows
1600 # variables. This is better than setup.cfg files because it allows
1600 # supplementing configs instead of replacing them.
1601 # supplementing configs instead of replacing them.
1601 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1602 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1602 if extrapackages:
1603 if extrapackages:
1603 py2exepackages.extend(extrapackages.split(' '))
1604 py2exepackages.extend(extrapackages.split(' '))
1604
1605
1605 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1606 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1606 if excludes:
1607 if excludes:
1607 py2exeexcludes.extend(excludes.split(' '))
1608 py2exeexcludes.extend(excludes.split(' '))
1608
1609
1609 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1610 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1610 if dllexcludes:
1611 if dllexcludes:
1611 py2exedllexcludes.extend(dllexcludes.split(' '))
1612 py2exedllexcludes.extend(dllexcludes.split(' '))
1612
1613
1613 if os.name == 'nt':
1614 if os.name == 'nt':
1614 # Windows binary file versions for exe/dll files must have the
1615 # Windows binary file versions for exe/dll files must have the
1615 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1616 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1616 setupversion = setupversion.split(r'+', 1)[0]
1617 setupversion = setupversion.split(r'+', 1)[0]
1617
1618
1618 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1619 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1619 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1620 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1620 if version:
1621 if version:
1621 version = version[0]
1622 version = version[0]
1622 if sys.version_info[0] == 3:
1623 if sys.version_info[0] == 3:
1623 version = version.decode('utf-8')
1624 version = version.decode('utf-8')
1624 xcode4 = version.startswith('Xcode') and StrictVersion(
1625 xcode4 = version.startswith('Xcode') and StrictVersion(
1625 version.split()[1]
1626 version.split()[1]
1626 ) >= StrictVersion('4.0')
1627 ) >= StrictVersion('4.0')
1627 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1628 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1628 else:
1629 else:
1629 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1630 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1630 # installed, but instead with only command-line tools. Assume
1631 # installed, but instead with only command-line tools. Assume
1631 # that only happens on >= Lion, thus no PPC support.
1632 # that only happens on >= Lion, thus no PPC support.
1632 xcode4 = True
1633 xcode4 = True
1633 xcode51 = False
1634 xcode51 = False
1634
1635
1635 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1636 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1636 # distutils.sysconfig
1637 # distutils.sysconfig
1637 if xcode4:
1638 if xcode4:
1638 os.environ['ARCHFLAGS'] = ''
1639 os.environ['ARCHFLAGS'] = ''
1639
1640
1640 # XCode 5.1 changes clang such that it now fails to compile if the
1641 # XCode 5.1 changes clang such that it now fails to compile if the
1641 # -mno-fused-madd flag is passed, but the version of Python shipped with
1642 # -mno-fused-madd flag is passed, but the version of Python shipped with
1642 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1643 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1643 # C extension modules, and a bug has been filed upstream at
1644 # C extension modules, and a bug has been filed upstream at
1644 # http://bugs.python.org/issue21244. We also need to patch this here
1645 # http://bugs.python.org/issue21244. We also need to patch this here
1645 # so Mercurial can continue to compile in the meantime.
1646 # so Mercurial can continue to compile in the meantime.
1646 if xcode51:
1647 if xcode51:
1647 cflags = get_config_var('CFLAGS')
1648 cflags = get_config_var('CFLAGS')
1648 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1649 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1649 os.environ['CFLAGS'] = (
1650 os.environ['CFLAGS'] = (
1650 os.environ.get('CFLAGS', '') + ' -Qunused-arguments'
1651 os.environ.get('CFLAGS', '') + ' -Qunused-arguments'
1651 )
1652 )
1652
1653
1653 setup(
1654 setup(
1654 name='mercurial',
1655 name='mercurial',
1655 version=setupversion,
1656 version=setupversion,
1656 author='Matt Mackall and many others',
1657 author='Matt Mackall and many others',
1657 author_email='mercurial@mercurial-scm.org',
1658 author_email='mercurial@mercurial-scm.org',
1658 url='https://mercurial-scm.org/',
1659 url='https://mercurial-scm.org/',
1659 download_url='https://mercurial-scm.org/release/',
1660 download_url='https://mercurial-scm.org/release/',
1660 description=(
1661 description=(
1661 'Fast scalable distributed SCM (revision control, version '
1662 'Fast scalable distributed SCM (revision control, version '
1662 'control) system'
1663 'control) system'
1663 ),
1664 ),
1664 long_description=(
1665 long_description=(
1665 'Mercurial is a distributed SCM tool written in Python.'
1666 'Mercurial is a distributed SCM tool written in Python.'
1666 ' It is used by a number of large projects that require'
1667 ' It is used by a number of large projects that require'
1667 ' fast, reliable distributed revision control, such as '
1668 ' fast, reliable distributed revision control, such as '
1668 'Mozilla.'
1669 'Mozilla.'
1669 ),
1670 ),
1670 license='GNU GPLv2 or any later version',
1671 license='GNU GPLv2 or any later version',
1671 classifiers=[
1672 classifiers=[
1672 'Development Status :: 6 - Mature',
1673 'Development Status :: 6 - Mature',
1673 'Environment :: Console',
1674 'Environment :: Console',
1674 'Intended Audience :: Developers',
1675 'Intended Audience :: Developers',
1675 'Intended Audience :: System Administrators',
1676 'Intended Audience :: System Administrators',
1676 'License :: OSI Approved :: GNU General Public License (GPL)',
1677 'License :: OSI Approved :: GNU General Public License (GPL)',
1677 'Natural Language :: Danish',
1678 'Natural Language :: Danish',
1678 'Natural Language :: English',
1679 'Natural Language :: English',
1679 'Natural Language :: German',
1680 'Natural Language :: German',
1680 'Natural Language :: Italian',
1681 'Natural Language :: Italian',
1681 'Natural Language :: Japanese',
1682 'Natural Language :: Japanese',
1682 'Natural Language :: Portuguese (Brazilian)',
1683 'Natural Language :: Portuguese (Brazilian)',
1683 'Operating System :: Microsoft :: Windows',
1684 'Operating System :: Microsoft :: Windows',
1684 'Operating System :: OS Independent',
1685 'Operating System :: OS Independent',
1685 'Operating System :: POSIX',
1686 'Operating System :: POSIX',
1686 'Programming Language :: C',
1687 'Programming Language :: C',
1687 'Programming Language :: Python',
1688 'Programming Language :: Python',
1688 'Topic :: Software Development :: Version Control',
1689 'Topic :: Software Development :: Version Control',
1689 ],
1690 ],
1690 scripts=scripts,
1691 scripts=scripts,
1691 packages=packages,
1692 packages=packages,
1692 ext_modules=extmodules,
1693 ext_modules=extmodules,
1693 data_files=datafiles,
1694 data_files=datafiles,
1694 package_data=packagedata,
1695 package_data=packagedata,
1695 cmdclass=cmdclass,
1696 cmdclass=cmdclass,
1696 distclass=hgdist,
1697 distclass=hgdist,
1697 options={
1698 options={
1698 'py2exe': {
1699 'py2exe': {
1699 'bundle_files': 3,
1700 'bundle_files': 3,
1700 'dll_excludes': py2exedllexcludes,
1701 'dll_excludes': py2exedllexcludes,
1701 'excludes': py2exeexcludes,
1702 'excludes': py2exeexcludes,
1702 'packages': py2exepackages,
1703 'packages': py2exepackages,
1703 },
1704 },
1704 'bdist_mpkg': {
1705 'bdist_mpkg': {
1705 'zipdist': False,
1706 'zipdist': False,
1706 'license': 'COPYING',
1707 'license': 'COPYING',
1707 'readme': 'contrib/packaging/macosx/Readme.html',
1708 'readme': 'contrib/packaging/macosx/Readme.html',
1708 'welcome': 'contrib/packaging/macosx/Welcome.html',
1709 'welcome': 'contrib/packaging/macosx/Welcome.html',
1709 },
1710 },
1710 },
1711 },
1711 **extra
1712 **extra
1712 )
1713 )
General Comments 0
You need to be logged in to leave comments. Login now