##// END OF EJS Templates
branching: merge with stable
marmoute -
r50994:0f0880c8 merge default
parent child Browse files
Show More
@@ -1,305 +1,305 b''
1 1 # If you want to change PREFIX, do not just edit it below. The changed
2 2 # value wont get passed on to recursive make calls. You should instead
3 3 # override the variable on the command like:
4 4 #
5 5 # % make PREFIX=/opt/ install
6 6
7 7 export PREFIX=/usr/local
8 8
9 9 # Default to Python 3.
10 10 #
11 11 # Windows ships Python 3 as `python.exe`, which may not be on PATH. py.exe is.
12 12 ifeq ($(OS),Windows_NT)
13 13 PYTHON?=py -3
14 14 else
15 15 PYTHON?=python3
16 16 endif
17 17
18 18 PYOXIDIZER?=pyoxidizer
19 19
20 20 $(eval HGROOT := $(shell pwd))
21 21 HGPYTHONS ?= $(HGROOT)/build/pythons
22 22 PURE=
23 23 PYFILESCMD=find mercurial hgext doc -name '*.py'
24 24 PYFILES:=$(shell $(PYFILESCMD))
25 25 DOCFILES=mercurial/helptext/*.txt
26 26 export LANGUAGE=C
27 27 export LC_ALL=C
28 28 TESTFLAGS ?= $(shell echo $$HGTESTFLAGS)
29 29 OSXVERSIONFLAGS ?= $(shell echo $$OSXVERSIONFLAGS)
30 30 CARGO = cargo
31 31
32 32 # Set this to e.g. "mingw32" to use a non-default compiler.
33 33 COMPILER=
34 34
35 35 COMPILERFLAG_tmp_ =
36 36 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
37 37 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
38 38
39 39 help:
40 40 @echo 'Commonly used make targets:'
41 41 @echo ' all - build program and documentation'
42 42 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
43 43 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
44 44 @echo ' local - build for inplace usage'
45 45 @echo ' tests - run all tests in the automatic test suite'
46 46 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
47 47 @echo ' dist - run all tests and create a source tarball in dist/'
48 48 @echo ' clean - remove files created by other targets'
49 49 @echo ' (except installed files or dist source tarball)'
50 50 @echo ' update-pot - update i18n/hg.pot'
51 51 @echo
52 52 @echo 'Example for a system-wide installation under /usr/local:'
53 53 @echo ' make all && su -c "make install" && hg version'
54 54 @echo
55 55 @echo 'Example for a local installation (usable in this directory):'
56 56 @echo ' make local && ./hg version'
57 57
58 58 all: build doc
59 59
60 60 local:
61 $(PYTHON) setup.py $(PURE) \
61 MERCURIAL_SETUP_MAKE_LOCAL=1 $(PYTHON) setup.py $(PURE) \
62 62 build_py -c -d . \
63 63 build_ext $(COMPILERFLAG) -i \
64 64 build_hgexe $(COMPILERFLAG) -i \
65 65 build_mo
66 66 env HGRCPATH= $(PYTHON) hg version
67 67
68 68 build:
69 69 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
70 70
71 71 build-chg:
72 72 make -C contrib/chg
73 73
74 74 build-rhg:
75 75 (cd rust/rhg; cargo build --release)
76 76
77 77 wheel:
78 78 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
79 79
80 80 doc:
81 81 $(MAKE) -C doc
82 82
83 83 cleanbutpackages:
84 84 rm -f hg.exe
85 85 -$(PYTHON) setup.py clean --all # ignore errors from this command
86 86 find contrib doc hgext hgext3rd i18n mercurial tests hgdemandimport \
87 87 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
88 88 rm -f MANIFEST MANIFEST.in hgext/__index__.py tests/*.err
89 89 rm -f mercurial/__modulepolicy__.py
90 90 if test -d .hg; then rm -f mercurial/__version__.py; fi
91 91 rm -rf build mercurial/locale
92 92 $(MAKE) -C doc clean
93 93 $(MAKE) -C contrib/chg distclean
94 94 rm -rf rust/target
95 95 rm -f mercurial/rustext.so
96 96
97 97 clean: cleanbutpackages
98 98 rm -rf packages
99 99
100 100 install: install-bin install-doc
101 101
102 102 install-bin: build
103 103 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
104 104
105 105 install-chg: build-chg
106 106 make -C contrib/chg install PREFIX="$(PREFIX)"
107 107
108 108 install-doc: doc
109 109 cd doc && $(MAKE) $(MFLAGS) install
110 110
111 111 install-home: install-home-bin install-home-doc
112 112
113 113 install-home-bin: build
114 114 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
115 115
116 116 install-home-doc: doc
117 117 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
118 118
119 119 install-rhg: build-rhg
120 120 install -m 755 rust/target/release/rhg "$(PREFIX)"/bin/
121 121
122 122 MANIFEST-doc:
123 123 $(MAKE) -C doc MANIFEST
124 124
125 125 MANIFEST.in: MANIFEST-doc
126 126 hg manifest | sed -e 's/^/include /' > MANIFEST.in
127 127 echo include mercurial/__version__.py >> MANIFEST.in
128 128 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
129 129
130 130 dist: tests dist-notests
131 131
132 132 dist-notests: doc MANIFEST.in
133 133 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
134 134
135 135 check: tests
136 136
137 137 tests:
138 138 # Run Rust tests if cargo is installed
139 139 if command -v $(CARGO) >/dev/null 2>&1; then \
140 140 $(MAKE) rust-tests; \
141 141 $(MAKE) cargo-clippy; \
142 142 fi
143 143 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
144 144
145 145 test-%:
146 146 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
147 147
148 148 testpy-%:
149 149 @echo Looking for Python $* in $(HGPYTHONS)
150 150 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
151 151 cd $$(mktemp --directory --tmpdir) && \
152 152 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
153 153 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
154 154
155 155 rust-tests:
156 156 cd $(HGROOT)/rust \
157 157 && $(CARGO) test --quiet --all --features "$(HG_RUST_FEATURES)"
158 158
159 159 cargo-clippy:
160 160 cd $(HGROOT)/rust \
161 161 && $(CARGO) clippy --all --features "$(HG_RUST_FEATURES)" -- -D warnings
162 162
163 163 check-code:
164 164 hg manifest | xargs python contrib/check-code.py
165 165
166 166 format-c:
167 167 clang-format --style file -i \
168 168 `hg files 'set:(**.c or **.cc or **.h) and not "listfile:contrib/clang-format-ignorelist"'`
169 169
170 170 update-pot: i18n/hg.pot
171 171
172 172 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
173 173 $(PYTHON) i18n/hggettext mercurial/commands.py \
174 174 hgext/*.py hgext/*/__init__.py \
175 175 mercurial/fileset.py mercurial/revset.py \
176 176 mercurial/templatefilters.py \
177 177 mercurial/templatefuncs.py \
178 178 mercurial/templatekw.py \
179 179 mercurial/filemerge.py \
180 180 mercurial/hgweb/webcommands.py \
181 181 mercurial/util.py \
182 182 $(DOCFILES) > i18n/hg.pot.tmp
183 183 # All strings marked for translation in Mercurial contain
184 184 # ASCII characters only. But some files contain string
185 185 # literals like this '\037\213'. xgettext thinks it has to
186 186 # parse them even though they are not marked for translation.
187 187 # Extracting with an explicit encoding of ISO-8859-1 will make
188 188 # xgettext "parse" and ignore them.
189 189 $(PYFILESCMD) | xargs \
190 190 xgettext --package-name "Mercurial" \
191 191 --msgid-bugs-address "<mercurial-devel@mercurial-scm.org>" \
192 192 --copyright-holder "Olivia Mackall <olivia@selenic.com> and others" \
193 193 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
194 194 -d hg -p i18n -o hg.pot.tmp
195 195 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
196 196 # The target file is not created before the last step. So it never is in
197 197 # an intermediate state.
198 198 mv -f i18n/hg.pot.tmp i18n/hg.pot
199 199
200 200 %.po: i18n/hg.pot
201 201 # work on a temporary copy for never having a half completed target
202 202 cp $@ $@.tmp
203 203 msgmerge --no-location --update $@.tmp $^
204 204 mv -f $@.tmp $@
205 205
206 206 # Packaging targets
207 207
208 208 packaging_targets := \
209 209 rhel7 \
210 210 rhel8 \
211 211 rhel9 \
212 212 deb \
213 213 docker-rhel7 \
214 214 docker-rhel8 \
215 215 docker-rhel9 \
216 216 docker-debian-bullseye \
217 217 docker-debian-buster \
218 218 docker-debian-stretch \
219 219 docker-fedora \
220 220 docker-ubuntu-xenial \
221 221 docker-ubuntu-xenial-ppa \
222 222 docker-ubuntu-bionic \
223 223 docker-ubuntu-bionic-ppa \
224 224 docker-ubuntu-focal \
225 225 docker-ubuntu-focal-ppa \
226 226 fedora \
227 227 linux-wheels \
228 228 linux-wheels-x86_64 \
229 229 linux-wheels-i686 \
230 230 ppa
231 231
232 232 # Forward packaging targets for convenience.
233 233 $(packaging_targets):
234 234 $(MAKE) -C contrib/packaging $@
235 235
236 236 osx:
237 237 rm -rf build/mercurial
238 238 /usr/bin/python2.7 setup.py install --optimize=1 \
239 239 --root=build/mercurial/ --prefix=/usr/local/ \
240 240 --install-lib=/Library/Python/2.7/site-packages/
241 241 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
242 242 # Place a bogon .DS_Store file in the target dir so we can be
243 243 # sure it doesn't get included in the final package.
244 244 touch build/mercurial/.DS_Store
245 245 make -C contrib/chg \
246 246 HGPATH=/usr/local/bin/hg \
247 247 PYTHON=/usr/bin/python2.7 \
248 248 DESTDIR=../../build/mercurial \
249 249 PREFIX=/usr/local \
250 250 clean install
251 251 mkdir -p $${OUTPUTDIR:-dist}
252 252 HGVER=$$(python contrib/genosxversion.py $(OSXVERSIONFLAGS) build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py) && \
253 253 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
254 254 pkgbuild --filter \\.DS_Store --root build/mercurial/ \
255 255 --identifier org.mercurial-scm.mercurial \
256 256 --version "$${HGVER}" \
257 257 build/mercurial.pkg && \
258 258 productbuild --distribution contrib/packaging/macosx/distribution.xml \
259 259 --package-path build/ \
260 260 --version "$${HGVER}" \
261 261 --resources contrib/packaging/macosx/ \
262 262 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
263 263
264 264 pyoxidizer:
265 265 $(PYOXIDIZER) build --path ./rust/hgcli --release
266 266
267 267
268 268 # a temporary target to setup all we need for run-tests.py --pyoxidizer
269 269 # (should go away as the run-tests implementation improves
270 270 pyoxidizer-windows-tests: PYOX_DIR=build/pyoxidizer/x86_64-pc-windows-msvc/release/app
271 271 pyoxidizer-windows-tests: pyoxidizer
272 272 rm -rf $(PYOX_DIR)/templates
273 273 cp -ar $(PYOX_DIR)/lib/mercurial/templates $(PYOX_DIR)/templates
274 274 rm -rf $(PYOX_DIR)/helptext
275 275 cp -ar $(PYOX_DIR)/lib/mercurial/helptext $(PYOX_DIR)/helptext
276 276 rm -rf $(PYOX_DIR)/defaultrc
277 277 cp -ar $(PYOX_DIR)/lib/mercurial/defaultrc $(PYOX_DIR)/defaultrc
278 278 rm -rf $(PYOX_DIR)/contrib
279 279 cp -ar contrib $(PYOX_DIR)/contrib
280 280 rm -rf $(PYOX_DIR)/doc
281 281 cp -ar doc $(PYOX_DIR)/doc
282 282
283 283
284 284 # a temporary target to setup all we need for run-tests.py --pyoxidizer
285 285 # (should go away as the run-tests implementation improves
286 286 pyoxidizer-macos-tests: PYOX_DIR=build/pyoxidizer/x86_64-apple-darwin/release/app
287 287 pyoxidizer-macos-tests: pyoxidizer
288 288 rm -rf $(PYOX_DIR)/templates
289 289 cp -a mercurial/templates $(PYOX_DIR)/templates
290 290 rm -rf $(PYOX_DIR)/helptext
291 291 cp -a mercurial/helptext $(PYOX_DIR)/helptext
292 292 rm -rf $(PYOX_DIR)/defaultrc
293 293 cp -a mercurial/defaultrc $(PYOX_DIR)/defaultrc
294 294 rm -rf $(PYOX_DIR)/contrib
295 295 cp -a contrib $(PYOX_DIR)/contrib
296 296 rm -rf $(PYOX_DIR)/doc
297 297 cp -a doc $(PYOX_DIR)/doc
298 298
299 299
300 300 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
301 301 install-doc install-home install-home-bin install-home-doc \
302 302 dist dist-notests check tests rust-tests check-code format-c \
303 303 update-pot pyoxidizer pyoxidizer-windows-tests pyoxidizer-macos-tests \
304 304 $(packaging_targets) \
305 305 osx
@@ -1,699 +1,701 b''
1 1 # dirstatemap.py
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6
7 7 from .i18n import _
8 8
9 9 from . import (
10 10 error,
11 11 pathutil,
12 12 policy,
13 13 txnutil,
14 14 util,
15 15 )
16 16
17 17 from .dirstateutils import (
18 18 docket as docketmod,
19 19 v2,
20 20 )
21 21
22 22 parsers = policy.importmod('parsers')
23 23 rustmod = policy.importrust('dirstate')
24 24
25 25 propertycache = util.propertycache
26 26
27 27 if rustmod is None:
28 28 DirstateItem = parsers.DirstateItem
29 29 else:
30 30 DirstateItem = rustmod.DirstateItem
31 31
32 32 rangemask = 0x7FFFFFFF
33 33
34 34
35 35 class _dirstatemapcommon:
36 36 """
37 37 Methods that are identical for both implementations of the dirstatemap
38 38 class, with and without Rust extensions enabled.
39 39 """
40 40
41 41 # please pytype
42 42
43 43 _map = None
44 44 copymap = None
45 45
46 46 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
47 47 self._use_dirstate_v2 = use_dirstate_v2
48 48 self._nodeconstants = nodeconstants
49 49 self._ui = ui
50 50 self._opener = opener
51 51 self._root = root
52 52 self._filename = b'dirstate'
53 53 self._nodelen = 20 # Also update Rust code when changing this!
54 54 self._parents = None
55 55 self._dirtyparents = False
56 56 self._docket = None
57 57
58 58 # for consistent view between _pl() and _read() invocations
59 59 self._pendingmode = None
60 60
61 61 def preload(self):
62 62 """Loads the underlying data, if it's not already loaded"""
63 63 self._map
64 64
65 65 def get(self, key, default=None):
66 66 return self._map.get(key, default)
67 67
68 68 def __len__(self):
69 69 return len(self._map)
70 70
71 71 def __iter__(self):
72 72 return iter(self._map)
73 73
74 74 def __contains__(self, key):
75 75 return key in self._map
76 76
77 77 def __getitem__(self, item):
78 78 return self._map[item]
79 79
80 80 ### disk interaction
81 81
82 82 def _opendirstatefile(self):
83 83 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
84 84 if self._pendingmode is not None and self._pendingmode != mode:
85 85 fp.close()
86 86 raise error.Abort(
87 87 _(b'working directory state may be changed parallelly')
88 88 )
89 89 self._pendingmode = mode
90 90 return fp
91 91
92 92 def _readdirstatefile(self, size=-1):
93 93 try:
94 94 with self._opendirstatefile() as fp:
95 95 return fp.read(size)
96 96 except FileNotFoundError:
97 97 # File doesn't exist, so the current state is empty
98 98 return b''
99 99
100 100 @property
101 101 def docket(self):
102 102 if not self._docket:
103 103 if not self._use_dirstate_v2:
104 104 raise error.ProgrammingError(
105 105 b'dirstate only has a docket in v2 format'
106 106 )
107 107 self._docket = docketmod.DirstateDocket.parse(
108 108 self._readdirstatefile(), self._nodeconstants
109 109 )
110 110 return self._docket
111 111
112 112 def write_v2_no_append(self, tr, st, meta, packed):
113 113 old_docket = self.docket
114 114 new_docket = docketmod.DirstateDocket.with_new_uuid(
115 115 self.parents(), len(packed), meta
116 116 )
117 if old_docket.uuid == new_docket.uuid:
118 raise error.ProgrammingError(b'dirstate docket name collision')
117 119 data_filename = new_docket.data_filename()
118 120 self._opener.write(data_filename, packed)
119 121 # tell the transaction that we are adding a new file
120 122 if tr is not None:
121 123 tr.addbackup(data_filename, location=b'plain')
122 124 # Write the new docket after the new data file has been
123 125 # written. Because `st` was opened with `atomictemp=True`,
124 126 # the actual `.hg/dirstate` file is only affected on close.
125 127 st.write(new_docket.serialize())
126 128 st.close()
127 129 # Remove the old data file after the new docket pointing to
128 130 # the new data file was written.
129 131 if old_docket.uuid:
130 132 data_filename = old_docket.data_filename()
131 133 if tr is not None:
132 134 tr.addbackup(data_filename, location=b'plain')
133 135 unlink = lambda _tr=None: self._opener.unlink(data_filename)
134 136 if tr:
135 137 category = b"dirstate-v2-clean-" + old_docket.uuid
136 138 tr.addpostclose(category, unlink)
137 139 else:
138 140 unlink()
139 141 self._docket = new_docket
140 142
141 143 ### reading/setting parents
142 144
143 145 def parents(self):
144 146 if not self._parents:
145 147 if self._use_dirstate_v2:
146 148 self._parents = self.docket.parents
147 149 else:
148 150 read_len = self._nodelen * 2
149 151 st = self._readdirstatefile(read_len)
150 152 l = len(st)
151 153 if l == read_len:
152 154 self._parents = (
153 155 st[: self._nodelen],
154 156 st[self._nodelen : 2 * self._nodelen],
155 157 )
156 158 elif l == 0:
157 159 self._parents = (
158 160 self._nodeconstants.nullid,
159 161 self._nodeconstants.nullid,
160 162 )
161 163 else:
162 164 raise error.Abort(
163 165 _(b'working directory state appears damaged!')
164 166 )
165 167
166 168 return self._parents
167 169
168 170
169 171 class dirstatemap(_dirstatemapcommon):
170 172 """Map encapsulating the dirstate's contents.
171 173
172 174 The dirstate contains the following state:
173 175
174 176 - `identity` is the identity of the dirstate file, which can be used to
175 177 detect when changes have occurred to the dirstate file.
176 178
177 179 - `parents` is a pair containing the parents of the working copy. The
178 180 parents are updated by calling `setparents`.
179 181
180 182 - the state map maps filenames to tuples of (state, mode, size, mtime),
181 183 where state is a single character representing 'normal', 'added',
182 184 'removed', or 'merged'. It is read by treating the dirstate as a
183 185 dict. File state is updated by calling various methods (see each
184 186 documentation for details):
185 187
186 188 - `reset_state`,
187 189 - `set_tracked`
188 190 - `set_untracked`
189 191 - `set_clean`
190 192 - `set_possibly_dirty`
191 193
192 194 - `copymap` maps destination filenames to their source filename.
193 195
194 196 The dirstate also provides the following views onto the state:
195 197
196 198 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
197 199 form that they appear as in the dirstate.
198 200
199 201 - `dirfoldmap` is a dict mapping normalized directory names to the
200 202 denormalized form that they appear as in the dirstate.
201 203 """
202 204
203 205 ### Core data storage and access
204 206
205 207 @propertycache
206 208 def _map(self):
207 209 self._map = {}
208 210 self.read()
209 211 return self._map
210 212
211 213 @propertycache
212 214 def copymap(self):
213 215 self.copymap = {}
214 216 self._map
215 217 return self.copymap
216 218
217 219 def clear(self):
218 220 self._map.clear()
219 221 self.copymap.clear()
220 222 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
221 223 util.clearcachedproperty(self, b"_dirs")
222 224 util.clearcachedproperty(self, b"_alldirs")
223 225 util.clearcachedproperty(self, b"filefoldmap")
224 226 util.clearcachedproperty(self, b"dirfoldmap")
225 227
226 228 def items(self):
227 229 return self._map.items()
228 230
229 231 # forward for python2,3 compat
230 232 iteritems = items
231 233
232 234 def debug_iter(self, all):
233 235 """
234 236 Return an iterator of (filename, state, mode, size, mtime) tuples
235 237
236 238 `all` is unused when Rust is not enabled
237 239 """
238 240 for (filename, item) in self.items():
239 241 yield (filename, item.state, item.mode, item.size, item.mtime)
240 242
241 243 def keys(self):
242 244 return self._map.keys()
243 245
244 246 ### reading/setting parents
245 247
246 248 def setparents(self, p1, p2, fold_p2=False):
247 249 self._parents = (p1, p2)
248 250 self._dirtyparents = True
249 251 copies = {}
250 252 if fold_p2:
251 253 for f, s in self._map.items():
252 254 # Discard "merged" markers when moving away from a merge state
253 255 if s.p2_info:
254 256 source = self.copymap.pop(f, None)
255 257 if source:
256 258 copies[f] = source
257 259 s.drop_merge_data()
258 260 return copies
259 261
260 262 ### disk interaction
261 263
262 264 def read(self):
263 265 # ignore HG_PENDING because identity is used only for writing
264 266 self.identity = util.filestat.frompath(
265 267 self._opener.join(self._filename)
266 268 )
267 269
268 270 if self._use_dirstate_v2:
269 271 if not self.docket.uuid:
270 272 return
271 273 st = self._opener.read(self.docket.data_filename())
272 274 else:
273 275 st = self._readdirstatefile()
274 276
275 277 if not st:
276 278 return
277 279
278 280 # TODO: adjust this estimate for dirstate-v2
279 281 if util.safehasattr(parsers, b'dict_new_presized'):
280 282 # Make an estimate of the number of files in the dirstate based on
281 283 # its size. This trades wasting some memory for avoiding costly
282 284 # resizes. Each entry have a prefix of 17 bytes followed by one or
283 285 # two path names. Studies on various large-scale real-world repositories
284 286 # found 54 bytes a reasonable upper limit for the average path names.
285 287 # Copy entries are ignored for the sake of this estimate.
286 288 self._map = parsers.dict_new_presized(len(st) // 71)
287 289
288 290 # Python's garbage collector triggers a GC each time a certain number
289 291 # of container objects (the number being defined by
290 292 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
291 293 # for each file in the dirstate. The C version then immediately marks
292 294 # them as not to be tracked by the collector. However, this has no
293 295 # effect on when GCs are triggered, only on what objects the GC looks
294 296 # into. This means that O(number of files) GCs are unavoidable.
295 297 # Depending on when in the process's lifetime the dirstate is parsed,
296 298 # this can get very expensive. As a workaround, disable GC while
297 299 # parsing the dirstate.
298 300 #
299 301 # (we cannot decorate the function directly since it is in a C module)
300 302 if self._use_dirstate_v2:
301 303 p = self.docket.parents
302 304 meta = self.docket.tree_metadata
303 305 parse_dirstate = util.nogc(v2.parse_dirstate)
304 306 parse_dirstate(self._map, self.copymap, st, meta)
305 307 else:
306 308 parse_dirstate = util.nogc(parsers.parse_dirstate)
307 309 p = parse_dirstate(self._map, self.copymap, st)
308 310 if not self._dirtyparents:
309 311 self.setparents(*p)
310 312
311 313 # Avoid excess attribute lookups by fast pathing certain checks
312 314 self.__contains__ = self._map.__contains__
313 315 self.__getitem__ = self._map.__getitem__
314 316 self.get = self._map.get
315 317
316 318 def write(self, tr, st):
317 319 if self._use_dirstate_v2:
318 320 packed, meta = v2.pack_dirstate(self._map, self.copymap)
319 321 self.write_v2_no_append(tr, st, meta, packed)
320 322 else:
321 323 packed = parsers.pack_dirstate(
322 324 self._map, self.copymap, self.parents()
323 325 )
324 326 st.write(packed)
325 327 st.close()
326 328 self._dirtyparents = False
327 329
328 330 @propertycache
329 331 def identity(self):
330 332 self._map
331 333 return self.identity
332 334
333 335 ### code related to maintaining and accessing "extra" property
334 336 # (e.g. "has_dir")
335 337
336 338 def _dirs_incr(self, filename, old_entry=None):
337 339 """increment the dirstate counter if applicable"""
338 340 if (
339 341 old_entry is None or old_entry.removed
340 342 ) and "_dirs" in self.__dict__:
341 343 self._dirs.addpath(filename)
342 344 if old_entry is None and "_alldirs" in self.__dict__:
343 345 self._alldirs.addpath(filename)
344 346
345 347 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
346 348 """decrement the dirstate counter if applicable"""
347 349 if old_entry is not None:
348 350 if "_dirs" in self.__dict__ and not old_entry.removed:
349 351 self._dirs.delpath(filename)
350 352 if "_alldirs" in self.__dict__ and not remove_variant:
351 353 self._alldirs.delpath(filename)
352 354 elif remove_variant and "_alldirs" in self.__dict__:
353 355 self._alldirs.addpath(filename)
354 356 if "filefoldmap" in self.__dict__:
355 357 normed = util.normcase(filename)
356 358 self.filefoldmap.pop(normed, None)
357 359
358 360 @propertycache
359 361 def filefoldmap(self):
360 362 """Returns a dictionary mapping normalized case paths to their
361 363 non-normalized versions.
362 364 """
363 365 try:
364 366 makefilefoldmap = parsers.make_file_foldmap
365 367 except AttributeError:
366 368 pass
367 369 else:
368 370 return makefilefoldmap(
369 371 self._map, util.normcasespec, util.normcasefallback
370 372 )
371 373
372 374 f = {}
373 375 normcase = util.normcase
374 376 for name, s in self._map.items():
375 377 if not s.removed:
376 378 f[normcase(name)] = name
377 379 f[b'.'] = b'.' # prevents useless util.fspath() invocation
378 380 return f
379 381
380 382 @propertycache
381 383 def dirfoldmap(self):
382 384 f = {}
383 385 normcase = util.normcase
384 386 for name in self._dirs:
385 387 f[normcase(name)] = name
386 388 return f
387 389
388 390 def hastrackeddir(self, d):
389 391 """
390 392 Returns True if the dirstate contains a tracked (not removed) file
391 393 in this directory.
392 394 """
393 395 return d in self._dirs
394 396
395 397 def hasdir(self, d):
396 398 """
397 399 Returns True if the dirstate contains a file (tracked or removed)
398 400 in this directory.
399 401 """
400 402 return d in self._alldirs
401 403
402 404 @propertycache
403 405 def _dirs(self):
404 406 return pathutil.dirs(self._map, only_tracked=True)
405 407
406 408 @propertycache
407 409 def _alldirs(self):
408 410 return pathutil.dirs(self._map)
409 411
410 412 ### code related to manipulation of entries and copy-sources
411 413
412 414 def reset_state(
413 415 self,
414 416 filename,
415 417 wc_tracked=False,
416 418 p1_tracked=False,
417 419 p2_info=False,
418 420 has_meaningful_mtime=True,
419 421 parentfiledata=None,
420 422 ):
421 423 """Set a entry to a given state, diregarding all previous state
422 424
423 425 This is to be used by the part of the dirstate API dedicated to
424 426 adjusting the dirstate after a update/merge.
425 427
426 428 note: calling this might result to no entry existing at all if the
427 429 dirstate map does not see any point at having one for this file
428 430 anymore.
429 431 """
430 432 # copy information are now outdated
431 433 # (maybe new information should be in directly passed to this function)
432 434 self.copymap.pop(filename, None)
433 435
434 436 if not (p1_tracked or p2_info or wc_tracked):
435 437 old_entry = self._map.get(filename)
436 438 self._drop_entry(filename)
437 439 self._dirs_decr(filename, old_entry=old_entry)
438 440 return
439 441
440 442 old_entry = self._map.get(filename)
441 443 self._dirs_incr(filename, old_entry)
442 444 entry = DirstateItem(
443 445 wc_tracked=wc_tracked,
444 446 p1_tracked=p1_tracked,
445 447 p2_info=p2_info,
446 448 has_meaningful_mtime=has_meaningful_mtime,
447 449 parentfiledata=parentfiledata,
448 450 )
449 451 self._map[filename] = entry
450 452
451 453 def set_tracked(self, filename):
452 454 new = False
453 455 entry = self.get(filename)
454 456 if entry is None:
455 457 self._dirs_incr(filename)
456 458 entry = DirstateItem(
457 459 wc_tracked=True,
458 460 )
459 461
460 462 self._map[filename] = entry
461 463 new = True
462 464 elif not entry.tracked:
463 465 self._dirs_incr(filename, entry)
464 466 entry.set_tracked()
465 467 self._refresh_entry(filename, entry)
466 468 new = True
467 469 else:
468 470 # XXX This is probably overkill for more case, but we need this to
469 471 # fully replace the `normallookup` call with `set_tracked` one.
470 472 # Consider smoothing this in the future.
471 473 entry.set_possibly_dirty()
472 474 self._refresh_entry(filename, entry)
473 475 return new
474 476
475 477 def set_untracked(self, f):
476 478 """Mark a file as no longer tracked in the dirstate map"""
477 479 entry = self.get(f)
478 480 if entry is None:
479 481 return False
480 482 else:
481 483 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
482 484 if not entry.p2_info:
483 485 self.copymap.pop(f, None)
484 486 entry.set_untracked()
485 487 self._refresh_entry(f, entry)
486 488 return True
487 489
488 490 def set_clean(self, filename, mode, size, mtime):
489 491 """mark a file as back to a clean state"""
490 492 entry = self[filename]
491 493 size = size & rangemask
492 494 entry.set_clean(mode, size, mtime)
493 495 self._refresh_entry(filename, entry)
494 496 self.copymap.pop(filename, None)
495 497
496 498 def set_possibly_dirty(self, filename):
497 499 """record that the current state of the file on disk is unknown"""
498 500 entry = self[filename]
499 501 entry.set_possibly_dirty()
500 502 self._refresh_entry(filename, entry)
501 503
502 504 def _refresh_entry(self, f, entry):
503 505 """record updated state of an entry"""
504 506 if not entry.any_tracked:
505 507 self._map.pop(f, None)
506 508
507 509 def _drop_entry(self, f):
508 510 """remove any entry for file f
509 511
510 512 This should also drop associated copy information
511 513
512 514 The fact we actually need to drop it is the responsability of the caller"""
513 515 self._map.pop(f, None)
514 516 self.copymap.pop(f, None)
515 517
516 518
517 519 if rustmod is not None:
518 520
519 521 class dirstatemap(_dirstatemapcommon):
520 522
521 523 ### Core data storage and access
522 524
523 525 @propertycache
524 526 def _map(self):
525 527 """
526 528 Fills the Dirstatemap when called.
527 529 """
528 530 # ignore HG_PENDING because identity is used only for writing
529 531 self.identity = util.filestat.frompath(
530 532 self._opener.join(self._filename)
531 533 )
532 534
533 535 if self._use_dirstate_v2:
534 536 if self.docket.uuid:
535 537 # TODO: use mmap when possible
536 538 data = self._opener.read(self.docket.data_filename())
537 539 else:
538 540 data = b''
539 541 self._map = rustmod.DirstateMap.new_v2(
540 542 data, self.docket.data_size, self.docket.tree_metadata
541 543 )
542 544 parents = self.docket.parents
543 545 else:
544 546 self._map, parents = rustmod.DirstateMap.new_v1(
545 547 self._readdirstatefile()
546 548 )
547 549
548 550 if parents and not self._dirtyparents:
549 551 self.setparents(*parents)
550 552
551 553 self.__contains__ = self._map.__contains__
552 554 self.__getitem__ = self._map.__getitem__
553 555 self.get = self._map.get
554 556 return self._map
555 557
556 558 @property
557 559 def copymap(self):
558 560 return self._map.copymap()
559 561
560 562 def debug_iter(self, all):
561 563 """
562 564 Return an iterator of (filename, state, mode, size, mtime) tuples
563 565
564 566 `all`: also include with `state == b' '` dirstate tree nodes that
565 567 don't have an associated `DirstateItem`.
566 568
567 569 """
568 570 return self._map.debug_iter(all)
569 571
570 572 def clear(self):
571 573 self._map.clear()
572 574 self.setparents(
573 575 self._nodeconstants.nullid, self._nodeconstants.nullid
574 576 )
575 577 util.clearcachedproperty(self, b"_dirs")
576 578 util.clearcachedproperty(self, b"_alldirs")
577 579 util.clearcachedproperty(self, b"dirfoldmap")
578 580
579 581 def items(self):
580 582 return self._map.items()
581 583
582 584 # forward for python2,3 compat
583 585 iteritems = items
584 586
585 587 def keys(self):
586 588 return iter(self._map)
587 589
588 590 ### reading/setting parents
589 591
590 592 def setparents(self, p1, p2, fold_p2=False):
591 593 self._parents = (p1, p2)
592 594 self._dirtyparents = True
593 595 copies = {}
594 596 if fold_p2:
595 597 copies = self._map.setparents_fixup()
596 598 return copies
597 599
598 600 ### disk interaction
599 601
600 602 @propertycache
601 603 def identity(self):
602 604 self._map
603 605 return self.identity
604 606
605 607 def write(self, tr, st):
606 608 if not self._use_dirstate_v2:
607 609 p1, p2 = self.parents()
608 610 packed = self._map.write_v1(p1, p2)
609 611 st.write(packed)
610 612 st.close()
611 613 self._dirtyparents = False
612 614 return
613 615
614 616 # We can only append to an existing data file if there is one
615 617 can_append = self.docket.uuid is not None
616 618 packed, meta, append = self._map.write_v2(can_append)
617 619 if append:
618 620 docket = self.docket
619 621 data_filename = docket.data_filename()
620 622 # We mark it for backup to make sure a future `hg rollback` (or
621 623 # `hg recover`?) call find the data it needs to restore a
622 624 # working repository.
623 625 #
624 626 # The backup can use a hardlink because the format is resistant
625 627 # to trailing "dead" data.
626 628 if tr is not None:
627 629 tr.addbackup(data_filename, location=b'plain')
628 630 with self._opener(data_filename, b'r+b') as fp:
629 631 fp.seek(docket.data_size)
630 632 assert fp.tell() == docket.data_size
631 633 written = fp.write(packed)
632 634 if written is not None: # py2 may return None
633 635 assert written == len(packed), (written, len(packed))
634 636 docket.data_size += len(packed)
635 637 docket.parents = self.parents()
636 638 docket.tree_metadata = meta
637 639 st.write(docket.serialize())
638 640 st.close()
639 641 else:
640 642 self.write_v2_no_append(tr, st, meta, packed)
641 643 # Reload from the newly-written file
642 644 util.clearcachedproperty(self, b"_map")
643 645 self._dirtyparents = False
644 646
645 647 ### code related to maintaining and accessing "extra" property
646 648 # (e.g. "has_dir")
647 649
648 650 @propertycache
649 651 def filefoldmap(self):
650 652 """Returns a dictionary mapping normalized case paths to their
651 653 non-normalized versions.
652 654 """
653 655 return self._map.filefoldmapasdict()
654 656
655 657 def hastrackeddir(self, d):
656 658 return self._map.hastrackeddir(d)
657 659
658 660 def hasdir(self, d):
659 661 return self._map.hasdir(d)
660 662
661 663 @propertycache
662 664 def dirfoldmap(self):
663 665 f = {}
664 666 normcase = util.normcase
665 667 for name in self._map.tracked_dirs():
666 668 f[normcase(name)] = name
667 669 return f
668 670
669 671 ### code related to manipulation of entries and copy-sources
670 672
671 673 def set_tracked(self, f):
672 674 return self._map.set_tracked(f)
673 675
674 676 def set_untracked(self, f):
675 677 return self._map.set_untracked(f)
676 678
677 679 def set_clean(self, filename, mode, size, mtime):
678 680 self._map.set_clean(filename, mode, size, mtime)
679 681
680 682 def set_possibly_dirty(self, f):
681 683 self._map.set_possibly_dirty(f)
682 684
683 685 def reset_state(
684 686 self,
685 687 filename,
686 688 wc_tracked=False,
687 689 p1_tracked=False,
688 690 p2_info=False,
689 691 has_meaningful_mtime=True,
690 692 parentfiledata=None,
691 693 ):
692 694 return self._map.reset_state(
693 695 filename,
694 696 wc_tracked,
695 697 p1_tracked,
696 698 p2_info,
697 699 has_meaningful_mtime,
698 700 parentfiledata,
699 701 )
@@ -1,1762 +1,1795 b''
1 1 #
2 2 # This is the mercurial setup script.
3 3 #
4 4 # 'python setup.py install', or
5 5 # 'python setup.py --help' for more options
6 6 import os
7 7
8 8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
9 9 # in bytestrings.
10 10 supportedpy = ','.join(
11 11 [
12 12 '>=3.6.2',
13 13 ]
14 14 )
15 15
16 16 import sys, platform
17 17 import sysconfig
18 18
19 19
20 20 def sysstr(s):
21 21 return s.decode('latin-1')
22 22
23 23
24 def eprint(*args, **kwargs):
25 kwargs['file'] = sys.stderr
26 print(*args, **kwargs)
27
28
24 29 import ssl
25 30
26 31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
27 32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
28 33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
29 34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
30 35 # support. At the mentioned commit, they were unconditionally defined.
31 36 _notset = object()
32 37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
33 38 if has_tlsv1_1 is _notset:
34 39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
35 40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
36 41 if has_tlsv1_2 is _notset:
37 42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
38 43 if not (has_tlsv1_1 or has_tlsv1_2):
39 44 error = """
40 45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
41 46 Please make sure that your Python installation was compiled against an OpenSSL
42 47 version enabling these features (likely this requires the OpenSSL version to
43 48 be at least 1.0.1).
44 49 """
45 50 print(error, file=sys.stderr)
46 51 sys.exit(1)
47 52
48 53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
49 54
50 55 # Solaris Python packaging brain damage
51 56 try:
52 57 import hashlib
53 58
54 59 sha = hashlib.sha1()
55 60 except ImportError:
56 61 try:
57 62 import sha
58 63
59 64 sha.sha # silence unused import warning
60 65 except ImportError:
61 66 raise SystemExit(
62 67 "Couldn't import standard hashlib (incomplete Python install)."
63 68 )
64 69
65 70 try:
66 71 import zlib
67 72
68 73 zlib.compressobj # silence unused import warning
69 74 except ImportError:
70 75 raise SystemExit(
71 76 "Couldn't import standard zlib (incomplete Python install)."
72 77 )
73 78
74 79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
75 80 isironpython = False
76 81 try:
77 82 isironpython = (
78 83 platform.python_implementation().lower().find("ironpython") != -1
79 84 )
80 85 except AttributeError:
81 86 pass
82 87
83 88 if isironpython:
84 89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
85 90 else:
86 91 try:
87 92 import bz2
88 93
89 94 bz2.BZ2Compressor # silence unused import warning
90 95 except ImportError:
91 96 raise SystemExit(
92 97 "Couldn't import standard bz2 (incomplete Python install)."
93 98 )
94 99
95 100 ispypy = "PyPy" in sys.version
96 101
97 102 import ctypes
98 103 import stat, subprocess, time
99 104 import re
100 105 import shutil
101 106 import tempfile
102 107
103 108 # We have issues with setuptools on some platforms and builders. Until
104 109 # those are resolved, setuptools is opt-in except for platforms where
105 110 # we don't have issues.
106 111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
107 112 if issetuptools:
108 113 from setuptools import setup
109 114 else:
110 115 from distutils.core import setup
111 116 from distutils.ccompiler import new_compiler
112 117 from distutils.core import Command, Extension
113 118 from distutils.dist import Distribution
114 119 from distutils.command.build import build
115 120 from distutils.command.build_ext import build_ext
116 121 from distutils.command.build_py import build_py
117 122 from distutils.command.build_scripts import build_scripts
118 123 from distutils.command.install import install
119 124 from distutils.command.install_lib import install_lib
120 125 from distutils.command.install_scripts import install_scripts
121 126 from distutils import log
122 127 from distutils.spawn import spawn, find_executable
123 128 from distutils import file_util
124 129 from distutils.errors import (
125 130 CCompilerError,
126 131 DistutilsError,
127 132 DistutilsExecError,
128 133 )
129 134 from distutils.sysconfig import get_python_inc
130 135
131 136
132 137 def write_if_changed(path, content):
133 138 """Write content to a file iff the content hasn't changed."""
134 139 if os.path.exists(path):
135 140 with open(path, 'rb') as fh:
136 141 current = fh.read()
137 142 else:
138 143 current = b''
139 144
140 145 if current != content:
141 146 with open(path, 'wb') as fh:
142 147 fh.write(content)
143 148
144 149
145 150 scripts = ['hg']
146 151 if os.name == 'nt':
147 152 # We remove hg.bat if we are able to build hg.exe.
148 153 scripts.append('contrib/win32/hg.bat')
149 154
150 155
151 156 def cancompile(cc, code):
152 157 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
153 158 devnull = oldstderr = None
154 159 try:
155 160 fname = os.path.join(tmpdir, 'testcomp.c')
156 161 f = open(fname, 'w')
157 162 f.write(code)
158 163 f.close()
159 164 # Redirect stderr to /dev/null to hide any error messages
160 165 # from the compiler.
161 166 # This will have to be changed if we ever have to check
162 167 # for a function on Windows.
163 168 devnull = open('/dev/null', 'w')
164 169 oldstderr = os.dup(sys.stderr.fileno())
165 170 os.dup2(devnull.fileno(), sys.stderr.fileno())
166 171 objects = cc.compile([fname], output_dir=tmpdir)
167 172 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
168 173 return True
169 174 except Exception:
170 175 return False
171 176 finally:
172 177 if oldstderr is not None:
173 178 os.dup2(oldstderr, sys.stderr.fileno())
174 179 if devnull is not None:
175 180 devnull.close()
176 181 shutil.rmtree(tmpdir)
177 182
178 183
179 184 # simplified version of distutils.ccompiler.CCompiler.has_function
180 185 # that actually removes its temporary files.
181 186 def hasfunction(cc, funcname):
182 187 code = 'int main(void) { %s(); }\n' % funcname
183 188 return cancompile(cc, code)
184 189
185 190
186 191 def hasheader(cc, headername):
187 192 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
188 193 return cancompile(cc, code)
189 194
190 195
191 196 # py2exe needs to be installed to work
192 197 try:
193 198 import py2exe
194 199
195 200 py2exe.patch_distutils()
196 201 py2exeloaded = True
197 202 # import py2exe's patched Distribution class
198 203 from distutils.core import Distribution
199 204 except ImportError:
200 205 py2exeloaded = False
201 206
202 207
203 208 def runcmd(cmd, env, cwd=None):
204 209 p = subprocess.Popen(
205 210 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
206 211 )
207 212 out, err = p.communicate()
208 213 return p.returncode, out, err
209 214
210 215
211 216 class hgcommand:
212 217 def __init__(self, cmd, env):
213 218 self.cmd = cmd
214 219 self.env = env
215 220
216 221 def run(self, args):
217 222 cmd = self.cmd + args
218 223 returncode, out, err = runcmd(cmd, self.env)
219 224 err = filterhgerr(err)
220 225 if err:
221 226 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
222 227 print(err, file=sys.stderr)
223 228 if returncode != 0:
224 229 return b''
225 230 return out
226 231
227 232
228 233 def filterhgerr(err):
229 234 # If root is executing setup.py, but the repository is owned by
230 235 # another user (as in "sudo python setup.py install") we will get
231 236 # trust warnings since the .hg/hgrc file is untrusted. That is
232 237 # fine, we don't want to load it anyway. Python may warn about
233 238 # a missing __init__.py in mercurial/locale, we also ignore that.
234 239 err = [
235 240 e
236 241 for e in err.splitlines()
237 242 if (
238 243 not e.startswith(b'not trusting file')
239 244 and not e.startswith(b'warning: Not importing')
240 245 and not e.startswith(b'obsolete feature not enabled')
241 246 and not e.startswith(b'*** failed to import extension')
242 247 and not e.startswith(b'devel-warn:')
243 248 and not (
244 249 e.startswith(b'(third party extension')
245 250 and e.endswith(b'or newer of Mercurial; disabling)')
246 251 )
247 252 )
248 253 ]
249 254 return b'\n'.join(b' ' + e for e in err)
250 255
251 256
252 257 def findhg():
253 258 """Try to figure out how we should invoke hg for examining the local
254 259 repository contents.
255 260
256 261 Returns an hgcommand object."""
257 262 # By default, prefer the "hg" command in the user's path. This was
258 263 # presumably the hg command that the user used to create this repository.
259 264 #
260 265 # This repository may require extensions or other settings that would not
261 266 # be enabled by running the hg script directly from this local repository.
262 267 hgenv = os.environ.copy()
263 268 # Use HGPLAIN to disable hgrc settings that would change output formatting,
264 269 # and disable localization for the same reasons.
265 270 hgenv['HGPLAIN'] = '1'
266 271 hgenv['LANGUAGE'] = 'C'
267 272 hgcmd = ['hg']
268 273 # Run a simple "hg log" command just to see if using hg from the user's
269 274 # path works and can successfully interact with this repository. Windows
270 275 # gives precedence to hg.exe in the current directory, so fall back to the
271 276 # python invocation of local hg, where pythonXY.dll can always be found.
272 277 check_cmd = ['log', '-r.', '-Ttest']
273 278 if os.name != 'nt' or not os.path.exists("hg.exe"):
274 279 try:
275 280 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
276 281 except EnvironmentError:
277 282 retcode = -1
278 283 if retcode == 0 and not filterhgerr(err):
279 284 return hgcommand(hgcmd, hgenv)
280 285
281 286 # Fall back to trying the local hg installation.
282 287 hgenv = localhgenv()
283 288 hgcmd = [sys.executable, 'hg']
284 289 try:
285 290 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
286 291 except EnvironmentError:
287 292 retcode = -1
288 293 if retcode == 0 and not filterhgerr(err):
289 294 return hgcommand(hgcmd, hgenv)
290 295
291 raise SystemExit(
292 'Unable to find a working hg binary to extract the '
293 'version from the repository tags'
294 )
296 eprint("/!\\")
297 eprint(r"/!\ Unable to find a working hg binary")
298 eprint(r"/!\ Version cannot be extract from the repository")
299 eprint(r"/!\ Re-run the setup once a first version is built")
300 return None
295 301
296 302
297 303 def localhgenv():
298 304 """Get an environment dictionary to use for invoking or importing
299 305 mercurial from the local repository."""
300 306 # Execute hg out of this directory with a custom environment which takes
301 307 # care to not use any hgrc files and do no localization.
302 308 env = {
303 309 'HGMODULEPOLICY': 'py',
304 310 'HGRCPATH': '',
305 311 'LANGUAGE': 'C',
306 312 'PATH': '',
307 313 } # make pypi modules that use os.environ['PATH'] happy
308 314 if 'LD_LIBRARY_PATH' in os.environ:
309 315 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
310 316 if 'SystemRoot' in os.environ:
311 317 # SystemRoot is required by Windows to load various DLLs. See:
312 318 # https://bugs.python.org/issue13524#msg148850
313 319 env['SystemRoot'] = os.environ['SystemRoot']
314 320 return env
315 321
316 322
317 323 version = ''
318 324
319 if os.path.isdir('.hg'):
325
326 def _try_get_version():
320 327 hg = findhg()
328 if hg is None:
329 return ''
330 hgid = None
331 numerictags = []
321 332 cmd = ['log', '-r', '.', '--template', '{tags}\n']
322 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
333 pieces = sysstr(hg.run(cmd)).split()
334 numerictags = [t for t in pieces if t[0:1].isdigit()]
323 335 hgid = sysstr(hg.run(['id', '-i'])).strip()
324 336 if not hgid:
325 # Bail out if hg is having problems interacting with this repository,
326 # rather than falling through and producing a bogus version number.
327 # Continuing with an invalid version number will break extensions
328 # that define minimumhgversion.
329 raise SystemExit('Unable to determine hg version from local repository')
337 eprint("/!\\")
338 eprint(r"/!\ Unable to determine hg version from local repository")
339 eprint(r"/!\ Failed to retrieve current revision tags")
340 return ''
330 341 if numerictags: # tag(s) found
331 342 version = numerictags[-1]
332 343 if hgid.endswith('+'): # propagate the dirty status to the tag
333 344 version += '+'
334 else: # no tag found
345 else: # no tag found on the checked out revision
335 346 ltagcmd = ['parents', '--template', '{latesttag}']
336 347 ltag = sysstr(hg.run(ltagcmd))
337 348 if not ltag:
338 ltag = 'null'
349 eprint("/!\\")
350 eprint(r"/!\ Unable to determine hg version from local repository")
351 eprint(
352 r"/!\ Failed to retrieve current revision distance to lated tag"
353 )
354 return ''
339 355 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
340 356 changessince = len(hg.run(changessincecmd).splitlines())
341 if ltag == 'null':
342 ltag = '0.0'
343 357 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
344 358 if version.endswith('+'):
345 359 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
360 return version
361
362
363 if os.path.isdir('.hg'):
364 version = _try_get_version()
346 365 elif os.path.exists('.hg_archival.txt'):
347 366 kw = dict(
348 367 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
349 368 )
350 369 if 'tag' in kw:
351 370 version = kw['tag']
352 371 elif 'latesttag' in kw:
353 372 if 'changessincelatesttag' in kw:
354 373 version = (
355 374 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
356 375 )
357 376 else:
358 377 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
359 378 else:
360 379 version = '0+hg' + kw.get('node', '')[:12]
361 380 elif os.path.exists('mercurial/__version__.py'):
362 381 with open('mercurial/__version__.py') as f:
363 382 data = f.read()
364 383 version = re.search('version = b"(.*)"', data).group(1)
365
366 if version:
367 versionb = version
368 if not isinstance(versionb, bytes):
369 versionb = versionb.encode('ascii')
384 if not version:
385 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
386 version = "0.0+0"
387 eprint("/!\\")
388 eprint(r"/!\ Using '0.0+0' as the default version")
389 eprint(r"/!\ Re-run make local once that first version is built")
390 eprint("/!\\")
391 else:
392 eprint("/!\\")
393 eprint(r"/!\ Could not determine the Mercurial version")
394 eprint(r"/!\ You need to build a local version first")
395 eprint(r"/!\ Run `make local` and try again")
396 eprint("/!\\")
397 msg = "Run `make local` first to get a working local version"
398 raise SystemExit(msg)
370 399
371 write_if_changed(
372 'mercurial/__version__.py',
373 b''.join(
374 [
375 b'# this file is autogenerated by setup.py\n'
376 b'version = b"%s"\n' % versionb,
377 ]
378 ),
379 )
400 versionb = version
401 if not isinstance(versionb, bytes):
402 versionb = versionb.encode('ascii')
403
404 write_if_changed(
405 'mercurial/__version__.py',
406 b''.join(
407 [
408 b'# this file is autogenerated by setup.py\n'
409 b'version = b"%s"\n' % versionb,
410 ]
411 ),
412 )
380 413
381 414
382 415 class hgbuild(build):
383 416 # Insert hgbuildmo first so that files in mercurial/locale/ are found
384 417 # when build_py is run next.
385 418 sub_commands = [('build_mo', None)] + build.sub_commands
386 419
387 420
388 421 class hgbuildmo(build):
389 422
390 423 description = "build translations (.mo files)"
391 424
392 425 def run(self):
393 426 if not find_executable('msgfmt'):
394 427 self.warn(
395 428 "could not find msgfmt executable, no translations "
396 429 "will be built"
397 430 )
398 431 return
399 432
400 433 podir = 'i18n'
401 434 if not os.path.isdir(podir):
402 435 self.warn("could not find %s/ directory" % podir)
403 436 return
404 437
405 438 join = os.path.join
406 439 for po in os.listdir(podir):
407 440 if not po.endswith('.po'):
408 441 continue
409 442 pofile = join(podir, po)
410 443 modir = join('locale', po[:-3], 'LC_MESSAGES')
411 444 mofile = join(modir, 'hg.mo')
412 445 mobuildfile = join('mercurial', mofile)
413 446 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
414 447 if sys.platform != 'sunos5':
415 448 # msgfmt on Solaris does not know about -c
416 449 cmd.append('-c')
417 450 self.mkpath(join('mercurial', modir))
418 451 self.make_file([pofile], mobuildfile, spawn, (cmd,))
419 452
420 453
421 454 class hgdist(Distribution):
422 455 pure = False
423 456 rust = False
424 457 no_rust = False
425 458 cffi = ispypy
426 459
427 460 global_options = Distribution.global_options + [
428 461 ('pure', None, "use pure (slow) Python code instead of C extensions"),
429 462 ('rust', None, "use Rust extensions additionally to C extensions"),
430 463 (
431 464 'no-rust',
432 465 None,
433 466 "do not use Rust extensions additionally to C extensions",
434 467 ),
435 468 ]
436 469
437 470 negative_opt = Distribution.negative_opt.copy()
438 471 boolean_options = ['pure', 'rust', 'no-rust']
439 472 negative_opt['no-rust'] = 'rust'
440 473
441 474 def _set_command_options(self, command_obj, option_dict=None):
442 475 # Not all distutils versions in the wild have boolean_options.
443 476 # This should be cleaned up when we're Python 3 only.
444 477 command_obj.boolean_options = (
445 478 getattr(command_obj, 'boolean_options', []) + self.boolean_options
446 479 )
447 480 return Distribution._set_command_options(
448 481 self, command_obj, option_dict=option_dict
449 482 )
450 483
451 484 def parse_command_line(self):
452 485 ret = Distribution.parse_command_line(self)
453 486 if not (self.rust or self.no_rust):
454 487 hgrustext = os.environ.get('HGWITHRUSTEXT')
455 488 # TODO record it for proper rebuild upon changes
456 489 # (see mercurial/__modulepolicy__.py)
457 490 if hgrustext != 'cpython' and hgrustext is not None:
458 491 if hgrustext:
459 492 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
460 493 print(msg, file=sys.stderr)
461 494 hgrustext = None
462 495 self.rust = hgrustext is not None
463 496 self.no_rust = not self.rust
464 497 return ret
465 498
466 499 def has_ext_modules(self):
467 500 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
468 501 # too late for some cases
469 502 return not self.pure and Distribution.has_ext_modules(self)
470 503
471 504
472 505 # This is ugly as a one-liner. So use a variable.
473 506 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
474 507 buildextnegops['no-zstd'] = 'zstd'
475 508 buildextnegops['no-rust'] = 'rust'
476 509
477 510
478 511 class hgbuildext(build_ext):
479 512 user_options = build_ext.user_options + [
480 513 ('zstd', None, 'compile zstd bindings [default]'),
481 514 ('no-zstd', None, 'do not compile zstd bindings'),
482 515 (
483 516 'rust',
484 517 None,
485 518 'compile Rust extensions if they are in use '
486 519 '(requires Cargo) [default]',
487 520 ),
488 521 ('no-rust', None, 'do not compile Rust extensions'),
489 522 ]
490 523
491 524 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
492 525 negative_opt = buildextnegops
493 526
494 527 def initialize_options(self):
495 528 self.zstd = True
496 529 self.rust = True
497 530
498 531 return build_ext.initialize_options(self)
499 532
500 533 def finalize_options(self):
501 534 # Unless overridden by the end user, build extensions in parallel.
502 535 # Only influences behavior on Python 3.5+.
503 536 if getattr(self, 'parallel', None) is None:
504 537 self.parallel = True
505 538
506 539 return build_ext.finalize_options(self)
507 540
508 541 def build_extensions(self):
509 542 ruststandalones = [
510 543 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
511 544 ]
512 545 self.extensions = [
513 546 e for e in self.extensions if e not in ruststandalones
514 547 ]
515 548 # Filter out zstd if disabled via argument.
516 549 if not self.zstd:
517 550 self.extensions = [
518 551 e for e in self.extensions if e.name != 'mercurial.zstd'
519 552 ]
520 553
521 554 # Build Rust standalone extensions if it'll be used
522 555 # and its build is not explicitly disabled (for external build
523 556 # as Linux distributions would do)
524 557 if self.distribution.rust and self.rust:
525 558 if not sys.platform.startswith('linux'):
526 559 self.warn(
527 560 "rust extensions have only been tested on Linux "
528 561 "and may not behave correctly on other platforms"
529 562 )
530 563
531 564 for rustext in ruststandalones:
532 565 rustext.build('' if self.inplace else self.build_lib)
533 566
534 567 return build_ext.build_extensions(self)
535 568
536 569 def build_extension(self, ext):
537 570 if (
538 571 self.distribution.rust
539 572 and self.rust
540 573 and isinstance(ext, RustExtension)
541 574 ):
542 575 ext.rustbuild()
543 576 try:
544 577 build_ext.build_extension(self, ext)
545 578 except CCompilerError:
546 579 if not getattr(ext, 'optional', False):
547 580 raise
548 581 log.warn(
549 582 "Failed to build optional extension '%s' (skipping)", ext.name
550 583 )
551 584
552 585
553 586 class hgbuildscripts(build_scripts):
554 587 def run(self):
555 588 if os.name != 'nt' or self.distribution.pure:
556 589 return build_scripts.run(self)
557 590
558 591 exebuilt = False
559 592 try:
560 593 self.run_command('build_hgexe')
561 594 exebuilt = True
562 595 except (DistutilsError, CCompilerError):
563 596 log.warn('failed to build optional hg.exe')
564 597
565 598 if exebuilt:
566 599 # Copying hg.exe to the scripts build directory ensures it is
567 600 # installed by the install_scripts command.
568 601 hgexecommand = self.get_finalized_command('build_hgexe')
569 602 dest = os.path.join(self.build_dir, 'hg.exe')
570 603 self.mkpath(self.build_dir)
571 604 self.copy_file(hgexecommand.hgexepath, dest)
572 605
573 606 # Remove hg.bat because it is redundant with hg.exe.
574 607 self.scripts.remove('contrib/win32/hg.bat')
575 608
576 609 return build_scripts.run(self)
577 610
578 611
579 612 class hgbuildpy(build_py):
580 613 def finalize_options(self):
581 614 build_py.finalize_options(self)
582 615
583 616 if self.distribution.pure:
584 617 self.distribution.ext_modules = []
585 618 elif self.distribution.cffi:
586 619 from mercurial.cffi import (
587 620 bdiffbuild,
588 621 mpatchbuild,
589 622 )
590 623
591 624 exts = [
592 625 mpatchbuild.ffi.distutils_extension(),
593 626 bdiffbuild.ffi.distutils_extension(),
594 627 ]
595 628 # cffi modules go here
596 629 if sys.platform == 'darwin':
597 630 from mercurial.cffi import osutilbuild
598 631
599 632 exts.append(osutilbuild.ffi.distutils_extension())
600 633 self.distribution.ext_modules = exts
601 634 else:
602 635 h = os.path.join(get_python_inc(), 'Python.h')
603 636 if not os.path.exists(h):
604 637 raise SystemExit(
605 638 'Python headers are required to build '
606 639 'Mercurial but weren\'t found in %s' % h
607 640 )
608 641
609 642 def run(self):
610 643 basepath = os.path.join(self.build_lib, 'mercurial')
611 644 self.mkpath(basepath)
612 645
613 646 rust = self.distribution.rust
614 647 if self.distribution.pure:
615 648 modulepolicy = 'py'
616 649 elif self.build_lib == '.':
617 650 # in-place build should run without rebuilding and Rust extensions
618 651 modulepolicy = 'rust+c-allow' if rust else 'allow'
619 652 else:
620 653 modulepolicy = 'rust+c' if rust else 'c'
621 654
622 655 content = b''.join(
623 656 [
624 657 b'# this file is autogenerated by setup.py\n',
625 658 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
626 659 ]
627 660 )
628 661 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
629 662
630 663 build_py.run(self)
631 664
632 665
633 666 class buildhgextindex(Command):
634 667 description = 'generate prebuilt index of hgext (for frozen package)'
635 668 user_options = []
636 669 _indexfilename = 'hgext/__index__.py'
637 670
638 671 def initialize_options(self):
639 672 pass
640 673
641 674 def finalize_options(self):
642 675 pass
643 676
644 677 def run(self):
645 678 if os.path.exists(self._indexfilename):
646 679 with open(self._indexfilename, 'w') as f:
647 680 f.write('# empty\n')
648 681
649 682 # here no extension enabled, disabled() lists up everything
650 683 code = (
651 684 'import pprint; from mercurial import extensions; '
652 685 'ext = extensions.disabled();'
653 686 'ext.pop("__index__", None);'
654 687 'pprint.pprint(ext)'
655 688 )
656 689 returncode, out, err = runcmd(
657 690 [sys.executable, '-c', code], localhgenv()
658 691 )
659 692 if err or returncode != 0:
660 693 raise DistutilsExecError(err)
661 694
662 695 with open(self._indexfilename, 'wb') as f:
663 696 f.write(b'# this file is autogenerated by setup.py\n')
664 697 f.write(b'docs = ')
665 698 f.write(out)
666 699
667 700
668 701 class buildhgexe(build_ext):
669 702 description = 'compile hg.exe from mercurial/exewrapper.c'
670 703
671 704 LONG_PATHS_MANIFEST = """\
672 705 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
673 706 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
674 707 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
675 708 <security>
676 709 <requestedPrivileges>
677 710 <requestedExecutionLevel
678 711 level="asInvoker"
679 712 uiAccess="false"
680 713 />
681 714 </requestedPrivileges>
682 715 </security>
683 716 </trustInfo>
684 717 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
685 718 <application>
686 719 <!-- Windows Vista -->
687 720 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
688 721 <!-- Windows 7 -->
689 722 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
690 723 <!-- Windows 8 -->
691 724 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
692 725 <!-- Windows 8.1 -->
693 726 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
694 727 <!-- Windows 10 and Windows 11 -->
695 728 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
696 729 </application>
697 730 </compatibility>
698 731 <application xmlns="urn:schemas-microsoft-com:asm.v3">
699 732 <windowsSettings
700 733 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
701 734 <ws2:longPathAware>true</ws2:longPathAware>
702 735 </windowsSettings>
703 736 </application>
704 737 <dependency>
705 738 <dependentAssembly>
706 739 <assemblyIdentity type="win32"
707 740 name="Microsoft.Windows.Common-Controls"
708 741 version="6.0.0.0"
709 742 processorArchitecture="*"
710 743 publicKeyToken="6595b64144ccf1df"
711 744 language="*" />
712 745 </dependentAssembly>
713 746 </dependency>
714 747 </assembly>
715 748 """
716 749
717 750 def initialize_options(self):
718 751 build_ext.initialize_options(self)
719 752
720 753 def build_extensions(self):
721 754 if os.name != 'nt':
722 755 return
723 756 if isinstance(self.compiler, HackedMingw32CCompiler):
724 757 self.compiler.compiler_so = self.compiler.compiler # no -mdll
725 758 self.compiler.dll_libraries = [] # no -lmsrvc90
726 759
727 760 pythonlib = None
728 761
729 762 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
730 763 self.hgtarget = os.path.join(dirname, 'hg')
731 764
732 765 if getattr(sys, 'dllhandle', None):
733 766 # Different Python installs can have different Python library
734 767 # names. e.g. the official CPython distribution uses pythonXY.dll
735 768 # and MinGW uses libpythonX.Y.dll.
736 769 _kernel32 = ctypes.windll.kernel32
737 770 _kernel32.GetModuleFileNameA.argtypes = [
738 771 ctypes.c_void_p,
739 772 ctypes.c_void_p,
740 773 ctypes.c_ulong,
741 774 ]
742 775 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
743 776 size = 1000
744 777 buf = ctypes.create_string_buffer(size + 1)
745 778 filelen = _kernel32.GetModuleFileNameA(
746 779 sys.dllhandle, ctypes.byref(buf), size
747 780 )
748 781
749 782 if filelen > 0 and filelen != size:
750 783 dllbasename = os.path.basename(buf.value)
751 784 if not dllbasename.lower().endswith(b'.dll'):
752 785 raise SystemExit(
753 786 'Python DLL does not end with .dll: %s' % dllbasename
754 787 )
755 788 pythonlib = dllbasename[:-4]
756 789
757 790 # Copy the pythonXY.dll next to the binary so that it runs
758 791 # without tampering with PATH.
759 792 dest = os.path.join(
760 793 os.path.dirname(self.hgtarget),
761 794 os.fsdecode(dllbasename),
762 795 )
763 796
764 797 if not os.path.exists(dest):
765 798 shutil.copy(buf.value, dest)
766 799
767 800 # Also overwrite python3.dll so that hgext.git is usable.
768 801 # TODO: also handle the MSYS flavor
769 802 python_x = os.path.join(
770 803 os.path.dirname(os.fsdecode(buf.value)),
771 804 "python3.dll",
772 805 )
773 806
774 807 if os.path.exists(python_x):
775 808 dest = os.path.join(
776 809 os.path.dirname(self.hgtarget),
777 810 os.path.basename(python_x),
778 811 )
779 812
780 813 shutil.copy(python_x, dest)
781 814
782 815 if not pythonlib:
783 816 log.warn(
784 817 'could not determine Python DLL filename; assuming pythonXY'
785 818 )
786 819
787 820 hv = sys.hexversion
788 821 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
789 822
790 823 log.info('using %s as Python library name' % pythonlib)
791 824 with open('mercurial/hgpythonlib.h', 'wb') as f:
792 825 f.write(b'/* this file is autogenerated by setup.py */\n')
793 826 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
794 827
795 828 objects = self.compiler.compile(
796 829 ['mercurial/exewrapper.c'],
797 830 output_dir=self.build_temp,
798 831 macros=[('_UNICODE', None), ('UNICODE', None)],
799 832 )
800 833 self.compiler.link_executable(
801 834 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
802 835 )
803 836
804 837 self.addlongpathsmanifest()
805 838
806 839 def addlongpathsmanifest(self):
807 840 """Add manifest pieces so that hg.exe understands long paths
808 841
809 842 Why resource #1 should be used for .exe manifests? I don't know and
810 843 wasn't able to find an explanation for mortals. But it seems to work.
811 844 """
812 845 exefname = self.compiler.executable_filename(self.hgtarget)
813 846 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
814 847 os.close(fdauto)
815 848 with open(manfname, 'w', encoding="UTF-8") as f:
816 849 f.write(self.LONG_PATHS_MANIFEST)
817 850 log.info("long paths manifest is written to '%s'" % manfname)
818 851 outputresource = '-outputresource:%s;#1' % exefname
819 852 log.info("running mt.exe to update hg.exe's manifest in-place")
820 853
821 854 self.spawn(
822 855 [
823 856 self.compiler.mt,
824 857 '-nologo',
825 858 '-manifest',
826 859 manfname,
827 860 outputresource,
828 861 ]
829 862 )
830 863 log.info("done updating hg.exe's manifest")
831 864 os.remove(manfname)
832 865
833 866 @property
834 867 def hgexepath(self):
835 868 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
836 869 return os.path.join(self.build_temp, dir, 'hg.exe')
837 870
838 871
839 872 class hgbuilddoc(Command):
840 873 description = 'build documentation'
841 874 user_options = [
842 875 ('man', None, 'generate man pages'),
843 876 ('html', None, 'generate html pages'),
844 877 ]
845 878
846 879 def initialize_options(self):
847 880 self.man = None
848 881 self.html = None
849 882
850 883 def finalize_options(self):
851 884 # If --man or --html are set, only generate what we're told to.
852 885 # Otherwise generate everything.
853 886 have_subset = self.man is not None or self.html is not None
854 887
855 888 if have_subset:
856 889 self.man = True if self.man else False
857 890 self.html = True if self.html else False
858 891 else:
859 892 self.man = True
860 893 self.html = True
861 894
862 895 def run(self):
863 896 def normalizecrlf(p):
864 897 with open(p, 'rb') as fh:
865 898 orig = fh.read()
866 899
867 900 if b'\r\n' not in orig:
868 901 return
869 902
870 903 log.info('normalizing %s to LF line endings' % p)
871 904 with open(p, 'wb') as fh:
872 905 fh.write(orig.replace(b'\r\n', b'\n'))
873 906
874 907 def gentxt(root):
875 908 txt = 'doc/%s.txt' % root
876 909 log.info('generating %s' % txt)
877 910 res, out, err = runcmd(
878 911 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
879 912 )
880 913 if res:
881 914 raise SystemExit(
882 915 'error running gendoc.py: %s'
883 916 % '\n'.join([sysstr(out), sysstr(err)])
884 917 )
885 918
886 919 with open(txt, 'wb') as fh:
887 920 fh.write(out)
888 921
889 922 def gengendoc(root):
890 923 gendoc = 'doc/%s.gendoc.txt' % root
891 924
892 925 log.info('generating %s' % gendoc)
893 926 res, out, err = runcmd(
894 927 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
895 928 os.environ,
896 929 cwd='doc',
897 930 )
898 931 if res:
899 932 raise SystemExit(
900 933 'error running gendoc: %s'
901 934 % '\n'.join([sysstr(out), sysstr(err)])
902 935 )
903 936
904 937 with open(gendoc, 'wb') as fh:
905 938 fh.write(out)
906 939
907 940 def genman(root):
908 941 log.info('generating doc/%s' % root)
909 942 res, out, err = runcmd(
910 943 [
911 944 sys.executable,
912 945 'runrst',
913 946 'hgmanpage',
914 947 '--halt',
915 948 'warning',
916 949 '--strip-elements-with-class',
917 950 'htmlonly',
918 951 '%s.txt' % root,
919 952 root,
920 953 ],
921 954 os.environ,
922 955 cwd='doc',
923 956 )
924 957 if res:
925 958 raise SystemExit(
926 959 'error running runrst: %s'
927 960 % '\n'.join([sysstr(out), sysstr(err)])
928 961 )
929 962
930 963 normalizecrlf('doc/%s' % root)
931 964
932 965 def genhtml(root):
933 966 log.info('generating doc/%s.html' % root)
934 967 res, out, err = runcmd(
935 968 [
936 969 sys.executable,
937 970 'runrst',
938 971 'html',
939 972 '--halt',
940 973 'warning',
941 974 '--link-stylesheet',
942 975 '--stylesheet-path',
943 976 'style.css',
944 977 '%s.txt' % root,
945 978 '%s.html' % root,
946 979 ],
947 980 os.environ,
948 981 cwd='doc',
949 982 )
950 983 if res:
951 984 raise SystemExit(
952 985 'error running runrst: %s'
953 986 % '\n'.join([sysstr(out), sysstr(err)])
954 987 )
955 988
956 989 normalizecrlf('doc/%s.html' % root)
957 990
958 991 # This logic is duplicated in doc/Makefile.
959 992 sources = {
960 993 f
961 994 for f in os.listdir('mercurial/helptext')
962 995 if re.search(r'[0-9]\.txt$', f)
963 996 }
964 997
965 998 # common.txt is a one-off.
966 999 gentxt('common')
967 1000
968 1001 for source in sorted(sources):
969 1002 assert source[-4:] == '.txt'
970 1003 root = source[:-4]
971 1004
972 1005 gentxt(root)
973 1006 gengendoc(root)
974 1007
975 1008 if self.man:
976 1009 genman(root)
977 1010 if self.html:
978 1011 genhtml(root)
979 1012
980 1013
981 1014 class hginstall(install):
982 1015
983 1016 user_options = install.user_options + [
984 1017 (
985 1018 'old-and-unmanageable',
986 1019 None,
987 1020 'noop, present for eggless setuptools compat',
988 1021 ),
989 1022 (
990 1023 'single-version-externally-managed',
991 1024 None,
992 1025 'noop, present for eggless setuptools compat',
993 1026 ),
994 1027 ]
995 1028
996 1029 sub_commands = install.sub_commands + [
997 1030 ('install_completion', lambda self: True)
998 1031 ]
999 1032
1000 1033 # Also helps setuptools not be sad while we refuse to create eggs.
1001 1034 single_version_externally_managed = True
1002 1035
1003 1036 def get_sub_commands(self):
1004 1037 # Screen out egg related commands to prevent egg generation. But allow
1005 1038 # mercurial.egg-info generation, since that is part of modern
1006 1039 # packaging.
1007 1040 excl = {'bdist_egg'}
1008 1041 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1009 1042
1010 1043
1011 1044 class hginstalllib(install_lib):
1012 1045 """
1013 1046 This is a specialization of install_lib that replaces the copy_file used
1014 1047 there so that it supports setting the mode of files after copying them,
1015 1048 instead of just preserving the mode that the files originally had. If your
1016 1049 system has a umask of something like 027, preserving the permissions when
1017 1050 copying will lead to a broken install.
1018 1051
1019 1052 Note that just passing keep_permissions=False to copy_file would be
1020 1053 insufficient, as it might still be applying a umask.
1021 1054 """
1022 1055
1023 1056 def run(self):
1024 1057 realcopyfile = file_util.copy_file
1025 1058
1026 1059 def copyfileandsetmode(*args, **kwargs):
1027 1060 src, dst = args[0], args[1]
1028 1061 dst, copied = realcopyfile(*args, **kwargs)
1029 1062 if copied:
1030 1063 st = os.stat(src)
1031 1064 # Persist executable bit (apply it to group and other if user
1032 1065 # has it)
1033 1066 if st[stat.ST_MODE] & stat.S_IXUSR:
1034 1067 setmode = int('0755', 8)
1035 1068 else:
1036 1069 setmode = int('0644', 8)
1037 1070 m = stat.S_IMODE(st[stat.ST_MODE])
1038 1071 m = (m & ~int('0777', 8)) | setmode
1039 1072 os.chmod(dst, m)
1040 1073
1041 1074 file_util.copy_file = copyfileandsetmode
1042 1075 try:
1043 1076 install_lib.run(self)
1044 1077 finally:
1045 1078 file_util.copy_file = realcopyfile
1046 1079
1047 1080
1048 1081 class hginstallscripts(install_scripts):
1049 1082 """
1050 1083 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1051 1084 the configured directory for modules. If possible, the path is made relative
1052 1085 to the directory for scripts.
1053 1086 """
1054 1087
1055 1088 def initialize_options(self):
1056 1089 install_scripts.initialize_options(self)
1057 1090
1058 1091 self.install_lib = None
1059 1092
1060 1093 def finalize_options(self):
1061 1094 install_scripts.finalize_options(self)
1062 1095 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1063 1096
1064 1097 def run(self):
1065 1098 install_scripts.run(self)
1066 1099
1067 1100 # It only makes sense to replace @LIBDIR@ with the install path if
1068 1101 # the install path is known. For wheels, the logic below calculates
1069 1102 # the libdir to be "../..". This is because the internal layout of a
1070 1103 # wheel archive looks like:
1071 1104 #
1072 1105 # mercurial-3.6.1.data/scripts/hg
1073 1106 # mercurial/__init__.py
1074 1107 #
1075 1108 # When installing wheels, the subdirectories of the "<pkg>.data"
1076 1109 # directory are translated to system local paths and files therein
1077 1110 # are copied in place. The mercurial/* files are installed into the
1078 1111 # site-packages directory. However, the site-packages directory
1079 1112 # isn't known until wheel install time. This means we have no clue
1080 1113 # at wheel generation time what the installed site-packages directory
1081 1114 # will be. And, wheels don't appear to provide the ability to register
1082 1115 # custom code to run during wheel installation. This all means that
1083 1116 # we can't reliably set the libdir in wheels: the default behavior
1084 1117 # of looking in sys.path must do.
1085 1118
1086 1119 if (
1087 1120 os.path.splitdrive(self.install_dir)[0]
1088 1121 != os.path.splitdrive(self.install_lib)[0]
1089 1122 ):
1090 1123 # can't make relative paths from one drive to another, so use an
1091 1124 # absolute path instead
1092 1125 libdir = self.install_lib
1093 1126 else:
1094 1127 libdir = os.path.relpath(self.install_lib, self.install_dir)
1095 1128
1096 1129 for outfile in self.outfiles:
1097 1130 with open(outfile, 'rb') as fp:
1098 1131 data = fp.read()
1099 1132
1100 1133 # skip binary files
1101 1134 if b'\0' in data:
1102 1135 continue
1103 1136
1104 1137 # During local installs, the shebang will be rewritten to the final
1105 1138 # install path. During wheel packaging, the shebang has a special
1106 1139 # value.
1107 1140 if data.startswith(b'#!python'):
1108 1141 log.info(
1109 1142 'not rewriting @LIBDIR@ in %s because install path '
1110 1143 'not known' % outfile
1111 1144 )
1112 1145 continue
1113 1146
1114 1147 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1115 1148 with open(outfile, 'wb') as fp:
1116 1149 fp.write(data)
1117 1150
1118 1151
1119 1152 class hginstallcompletion(Command):
1120 1153 description = 'Install shell completion'
1121 1154
1122 1155 def initialize_options(self):
1123 1156 self.install_dir = None
1124 1157 self.outputs = []
1125 1158
1126 1159 def finalize_options(self):
1127 1160 self.set_undefined_options(
1128 1161 'install_data', ('install_dir', 'install_dir')
1129 1162 )
1130 1163
1131 1164 def get_outputs(self):
1132 1165 return self.outputs
1133 1166
1134 1167 def run(self):
1135 1168 for src, dir_path, dest in (
1136 1169 (
1137 1170 'bash_completion',
1138 1171 ('share', 'bash-completion', 'completions'),
1139 1172 'hg',
1140 1173 ),
1141 1174 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1142 1175 ):
1143 1176 dir = os.path.join(self.install_dir, *dir_path)
1144 1177 self.mkpath(dir)
1145 1178
1146 1179 dest = os.path.join(dir, dest)
1147 1180 self.outputs.append(dest)
1148 1181 self.copy_file(os.path.join('contrib', src), dest)
1149 1182
1150 1183
1151 1184 # virtualenv installs custom distutils/__init__.py and
1152 1185 # distutils/distutils.cfg files which essentially proxy back to the
1153 1186 # "real" distutils in the main Python install. The presence of this
1154 1187 # directory causes py2exe to pick up the "hacked" distutils package
1155 1188 # from the virtualenv and "import distutils" will fail from the py2exe
1156 1189 # build because the "real" distutils files can't be located.
1157 1190 #
1158 1191 # We work around this by monkeypatching the py2exe code finding Python
1159 1192 # modules to replace the found virtualenv distutils modules with the
1160 1193 # original versions via filesystem scanning. This is a bit hacky. But
1161 1194 # it allows us to use virtualenvs for py2exe packaging, which is more
1162 1195 # deterministic and reproducible.
1163 1196 #
1164 1197 # It's worth noting that the common StackOverflow suggestions for this
1165 1198 # problem involve copying the original distutils files into the
1166 1199 # virtualenv or into the staging directory after setup() is invoked.
1167 1200 # The former is very brittle and can easily break setup(). Our hacking
1168 1201 # of the found modules routine has a similar result as copying the files
1169 1202 # manually. But it makes fewer assumptions about how py2exe works and
1170 1203 # is less brittle.
1171 1204
1172 1205 # This only catches virtualenvs made with virtualenv (as opposed to
1173 1206 # venv, which is likely what Python 3 uses).
1174 1207 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1175 1208
1176 1209 if py2exehacked:
1177 1210 from distutils.command.py2exe import py2exe as buildpy2exe
1178 1211 from py2exe.mf import Module as py2exemodule
1179 1212
1180 1213 class hgbuildpy2exe(buildpy2exe):
1181 1214 def find_needed_modules(self, mf, files, modules):
1182 1215 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1183 1216
1184 1217 # Replace virtualenv's distutils modules with the real ones.
1185 1218 modules = {}
1186 1219 for k, v in res.modules.items():
1187 1220 if k != 'distutils' and not k.startswith('distutils.'):
1188 1221 modules[k] = v
1189 1222
1190 1223 res.modules = modules
1191 1224
1192 1225 import opcode
1193 1226
1194 1227 distutilsreal = os.path.join(
1195 1228 os.path.dirname(opcode.__file__), 'distutils'
1196 1229 )
1197 1230
1198 1231 for root, dirs, files in os.walk(distutilsreal):
1199 1232 for f in sorted(files):
1200 1233 if not f.endswith('.py'):
1201 1234 continue
1202 1235
1203 1236 full = os.path.join(root, f)
1204 1237
1205 1238 parents = ['distutils']
1206 1239
1207 1240 if root != distutilsreal:
1208 1241 rel = os.path.relpath(root, distutilsreal)
1209 1242 parents.extend(p for p in rel.split(os.sep))
1210 1243
1211 1244 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1212 1245
1213 1246 if modname.startswith('distutils.tests.'):
1214 1247 continue
1215 1248
1216 1249 if modname.endswith('.__init__'):
1217 1250 modname = modname[: -len('.__init__')]
1218 1251 path = os.path.dirname(full)
1219 1252 else:
1220 1253 path = None
1221 1254
1222 1255 res.modules[modname] = py2exemodule(
1223 1256 modname, full, path=path
1224 1257 )
1225 1258
1226 1259 if 'distutils' not in res.modules:
1227 1260 raise SystemExit('could not find distutils modules')
1228 1261
1229 1262 return res
1230 1263
1231 1264
1232 1265 cmdclass = {
1233 1266 'build': hgbuild,
1234 1267 'build_doc': hgbuilddoc,
1235 1268 'build_mo': hgbuildmo,
1236 1269 'build_ext': hgbuildext,
1237 1270 'build_py': hgbuildpy,
1238 1271 'build_scripts': hgbuildscripts,
1239 1272 'build_hgextindex': buildhgextindex,
1240 1273 'install': hginstall,
1241 1274 'install_completion': hginstallcompletion,
1242 1275 'install_lib': hginstalllib,
1243 1276 'install_scripts': hginstallscripts,
1244 1277 'build_hgexe': buildhgexe,
1245 1278 }
1246 1279
1247 1280 if py2exehacked:
1248 1281 cmdclass['py2exe'] = hgbuildpy2exe
1249 1282
1250 1283 packages = [
1251 1284 'mercurial',
1252 1285 'mercurial.cext',
1253 1286 'mercurial.cffi',
1254 1287 'mercurial.defaultrc',
1255 1288 'mercurial.dirstateutils',
1256 1289 'mercurial.helptext',
1257 1290 'mercurial.helptext.internals',
1258 1291 'mercurial.hgweb',
1259 1292 'mercurial.interfaces',
1260 1293 'mercurial.pure',
1261 1294 'mercurial.templates',
1262 1295 'mercurial.thirdparty',
1263 1296 'mercurial.thirdparty.attr',
1264 1297 'mercurial.thirdparty.zope',
1265 1298 'mercurial.thirdparty.zope.interface',
1266 1299 'mercurial.upgrade_utils',
1267 1300 'mercurial.utils',
1268 1301 'mercurial.revlogutils',
1269 1302 'mercurial.testing',
1270 1303 'hgext',
1271 1304 'hgext.convert',
1272 1305 'hgext.fsmonitor',
1273 1306 'hgext.fastannotate',
1274 1307 'hgext.fsmonitor.pywatchman',
1275 1308 'hgext.git',
1276 1309 'hgext.highlight',
1277 1310 'hgext.hooklib',
1278 1311 'hgext.infinitepush',
1279 1312 'hgext.largefiles',
1280 1313 'hgext.lfs',
1281 1314 'hgext.narrow',
1282 1315 'hgext.remotefilelog',
1283 1316 'hgext.zeroconf',
1284 1317 'hgext3rd',
1285 1318 'hgdemandimport',
1286 1319 ]
1287 1320
1288 1321 for name in os.listdir(os.path.join('mercurial', 'templates')):
1289 1322 if name != '__pycache__' and os.path.isdir(
1290 1323 os.path.join('mercurial', 'templates', name)
1291 1324 ):
1292 1325 packages.append('mercurial.templates.%s' % name)
1293 1326
1294 1327 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1295 1328 # py2exe can't cope with namespace packages very well, so we have to
1296 1329 # install any hgext3rd.* extensions that we want in the final py2exe
1297 1330 # image here. This is gross, but you gotta do what you gotta do.
1298 1331 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1299 1332
1300 1333 common_depends = [
1301 1334 'mercurial/bitmanipulation.h',
1302 1335 'mercurial/compat.h',
1303 1336 'mercurial/cext/util.h',
1304 1337 ]
1305 1338 common_include_dirs = ['mercurial']
1306 1339
1307 1340 common_cflags = []
1308 1341
1309 1342 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1310 1343 # makes declarations not at the top of a scope in the headers.
1311 1344 if os.name != 'nt' and sys.version_info[1] < 9:
1312 1345 common_cflags = ['-Werror=declaration-after-statement']
1313 1346
1314 1347 osutil_cflags = []
1315 1348 osutil_ldflags = []
1316 1349
1317 1350 # platform specific macros
1318 1351 for plat, func in [('bsd', 'setproctitle')]:
1319 1352 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1320 1353 osutil_cflags.append('-DHAVE_%s' % func.upper())
1321 1354
1322 1355 for plat, macro, code in [
1323 1356 (
1324 1357 'bsd|darwin',
1325 1358 'BSD_STATFS',
1326 1359 '''
1327 1360 #include <sys/param.h>
1328 1361 #include <sys/mount.h>
1329 1362 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1330 1363 ''',
1331 1364 ),
1332 1365 (
1333 1366 'linux',
1334 1367 'LINUX_STATFS',
1335 1368 '''
1336 1369 #include <linux/magic.h>
1337 1370 #include <sys/vfs.h>
1338 1371 int main() { struct statfs s; return sizeof(s.f_type); }
1339 1372 ''',
1340 1373 ),
1341 1374 ]:
1342 1375 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1343 1376 osutil_cflags.append('-DHAVE_%s' % macro)
1344 1377
1345 1378 if sys.platform == 'darwin':
1346 1379 osutil_ldflags += ['-framework', 'ApplicationServices']
1347 1380
1348 1381 if sys.platform == 'sunos5':
1349 1382 osutil_ldflags += ['-lsocket']
1350 1383
1351 1384 xdiff_srcs = [
1352 1385 'mercurial/thirdparty/xdiff/xdiffi.c',
1353 1386 'mercurial/thirdparty/xdiff/xprepare.c',
1354 1387 'mercurial/thirdparty/xdiff/xutils.c',
1355 1388 ]
1356 1389
1357 1390 xdiff_headers = [
1358 1391 'mercurial/thirdparty/xdiff/xdiff.h',
1359 1392 'mercurial/thirdparty/xdiff/xdiffi.h',
1360 1393 'mercurial/thirdparty/xdiff/xinclude.h',
1361 1394 'mercurial/thirdparty/xdiff/xmacros.h',
1362 1395 'mercurial/thirdparty/xdiff/xprepare.h',
1363 1396 'mercurial/thirdparty/xdiff/xtypes.h',
1364 1397 'mercurial/thirdparty/xdiff/xutils.h',
1365 1398 ]
1366 1399
1367 1400
1368 1401 class RustCompilationError(CCompilerError):
1369 1402 """Exception class for Rust compilation errors."""
1370 1403
1371 1404
1372 1405 class RustExtension(Extension):
1373 1406 """Base classes for concrete Rust Extension classes."""
1374 1407
1375 1408 rusttargetdir = os.path.join('rust', 'target', 'release')
1376 1409
1377 1410 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1378 1411 Extension.__init__(self, mpath, sources, **kw)
1379 1412 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1380 1413
1381 1414 # adding Rust source and control files to depends so that the extension
1382 1415 # gets rebuilt if they've changed
1383 1416 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1384 1417 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1385 1418 if os.path.exists(cargo_lock):
1386 1419 self.depends.append(cargo_lock)
1387 1420 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1388 1421 self.depends.extend(
1389 1422 os.path.join(dirpath, fname)
1390 1423 for fname in fnames
1391 1424 if os.path.splitext(fname)[1] == '.rs'
1392 1425 )
1393 1426
1394 1427 @staticmethod
1395 1428 def rustdylibsuffix():
1396 1429 """Return the suffix for shared libraries produced by rustc.
1397 1430
1398 1431 See also: https://doc.rust-lang.org/reference/linkage.html
1399 1432 """
1400 1433 if sys.platform == 'darwin':
1401 1434 return '.dylib'
1402 1435 elif os.name == 'nt':
1403 1436 return '.dll'
1404 1437 else:
1405 1438 return '.so'
1406 1439
1407 1440 def rustbuild(self):
1408 1441 env = os.environ.copy()
1409 1442 if 'HGTEST_RESTOREENV' in env:
1410 1443 # Mercurial tests change HOME to a temporary directory,
1411 1444 # but, if installed with rustup, the Rust toolchain needs
1412 1445 # HOME to be correct (otherwise the 'no default toolchain'
1413 1446 # error message is issued and the build fails).
1414 1447 # This happens currently with test-hghave.t, which does
1415 1448 # invoke this build.
1416 1449
1417 1450 # Unix only fix (os.path.expanduser not really reliable if
1418 1451 # HOME is shadowed like this)
1419 1452 import pwd
1420 1453
1421 1454 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1422 1455
1423 1456 cargocmd = ['cargo', 'rustc', '--release']
1424 1457
1425 1458 rust_features = env.get("HG_RUST_FEATURES")
1426 1459 if rust_features:
1427 1460 cargocmd.extend(('--features', rust_features))
1428 1461
1429 1462 cargocmd.append('--')
1430 1463 if sys.platform == 'darwin':
1431 1464 cargocmd.extend(
1432 1465 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1433 1466 )
1434 1467 try:
1435 1468 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1436 1469 except FileNotFoundError:
1437 1470 raise RustCompilationError("Cargo not found")
1438 1471 except PermissionError:
1439 1472 raise RustCompilationError(
1440 1473 "Cargo found, but permission to execute it is denied"
1441 1474 )
1442 1475 except subprocess.CalledProcessError:
1443 1476 raise RustCompilationError(
1444 1477 "Cargo failed. Working directory: %r, "
1445 1478 "command: %r, environment: %r"
1446 1479 % (self.rustsrcdir, cargocmd, env)
1447 1480 )
1448 1481
1449 1482
1450 1483 class RustStandaloneExtension(RustExtension):
1451 1484 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1452 1485 RustExtension.__init__(
1453 1486 self, pydottedname, [], dylibname, rustcrate, **kw
1454 1487 )
1455 1488 self.dylibname = dylibname
1456 1489
1457 1490 def build(self, target_dir):
1458 1491 self.rustbuild()
1459 1492 target = [target_dir]
1460 1493 target.extend(self.name.split('.'))
1461 1494 target[-1] += DYLIB_SUFFIX
1462 1495 target = os.path.join(*target)
1463 1496 os.makedirs(os.path.dirname(target), exist_ok=True)
1464 1497 shutil.copy2(
1465 1498 os.path.join(
1466 1499 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1467 1500 ),
1468 1501 target,
1469 1502 )
1470 1503
1471 1504
1472 1505 extmodules = [
1473 1506 Extension(
1474 1507 'mercurial.cext.base85',
1475 1508 ['mercurial/cext/base85.c'],
1476 1509 include_dirs=common_include_dirs,
1477 1510 extra_compile_args=common_cflags,
1478 1511 depends=common_depends,
1479 1512 ),
1480 1513 Extension(
1481 1514 'mercurial.cext.bdiff',
1482 1515 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1483 1516 include_dirs=common_include_dirs,
1484 1517 extra_compile_args=common_cflags,
1485 1518 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1486 1519 ),
1487 1520 Extension(
1488 1521 'mercurial.cext.mpatch',
1489 1522 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1490 1523 include_dirs=common_include_dirs,
1491 1524 extra_compile_args=common_cflags,
1492 1525 depends=common_depends,
1493 1526 ),
1494 1527 Extension(
1495 1528 'mercurial.cext.parsers',
1496 1529 [
1497 1530 'mercurial/cext/charencode.c',
1498 1531 'mercurial/cext/dirs.c',
1499 1532 'mercurial/cext/manifest.c',
1500 1533 'mercurial/cext/parsers.c',
1501 1534 'mercurial/cext/pathencode.c',
1502 1535 'mercurial/cext/revlog.c',
1503 1536 ],
1504 1537 include_dirs=common_include_dirs,
1505 1538 extra_compile_args=common_cflags,
1506 1539 depends=common_depends
1507 1540 + [
1508 1541 'mercurial/cext/charencode.h',
1509 1542 'mercurial/cext/revlog.h',
1510 1543 ],
1511 1544 ),
1512 1545 Extension(
1513 1546 'mercurial.cext.osutil',
1514 1547 ['mercurial/cext/osutil.c'],
1515 1548 include_dirs=common_include_dirs,
1516 1549 extra_compile_args=common_cflags + osutil_cflags,
1517 1550 extra_link_args=osutil_ldflags,
1518 1551 depends=common_depends,
1519 1552 ),
1520 1553 Extension(
1521 1554 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1522 1555 [
1523 1556 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1524 1557 ],
1525 1558 extra_compile_args=common_cflags,
1526 1559 ),
1527 1560 Extension(
1528 1561 'mercurial.thirdparty.sha1dc',
1529 1562 [
1530 1563 'mercurial/thirdparty/sha1dc/cext.c',
1531 1564 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1532 1565 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1533 1566 ],
1534 1567 extra_compile_args=common_cflags,
1535 1568 ),
1536 1569 Extension(
1537 1570 'hgext.fsmonitor.pywatchman.bser',
1538 1571 ['hgext/fsmonitor/pywatchman/bser.c'],
1539 1572 extra_compile_args=common_cflags,
1540 1573 ),
1541 1574 RustStandaloneExtension(
1542 1575 'mercurial.rustext',
1543 1576 'hg-cpython',
1544 1577 'librusthg',
1545 1578 ),
1546 1579 ]
1547 1580
1548 1581
1549 1582 sys.path.insert(0, 'contrib/python-zstandard')
1550 1583 import setup_zstd
1551 1584
1552 1585 zstd = setup_zstd.get_c_extension(
1553 1586 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1554 1587 )
1555 1588 zstd.extra_compile_args += common_cflags
1556 1589 extmodules.append(zstd)
1557 1590
1558 1591 try:
1559 1592 from distutils import cygwinccompiler
1560 1593
1561 1594 # the -mno-cygwin option has been deprecated for years
1562 1595 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1563 1596
1564 1597 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1565 1598 def __init__(self, *args, **kwargs):
1566 1599 mingw32compilerclass.__init__(self, *args, **kwargs)
1567 1600 for i in 'compiler compiler_so linker_exe linker_so'.split():
1568 1601 try:
1569 1602 getattr(self, i).remove('-mno-cygwin')
1570 1603 except ValueError:
1571 1604 pass
1572 1605
1573 1606 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1574 1607 except ImportError:
1575 1608 # the cygwinccompiler package is not available on some Python
1576 1609 # distributions like the ones from the optware project for Synology
1577 1610 # DiskStation boxes
1578 1611 class HackedMingw32CCompiler:
1579 1612 pass
1580 1613
1581 1614
1582 1615 if os.name == 'nt':
1583 1616 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1584 1617 # extra_link_args to distutils.extensions.Extension() doesn't have any
1585 1618 # effect.
1586 1619 from distutils import msvccompiler
1587 1620
1588 1621 msvccompilerclass = msvccompiler.MSVCCompiler
1589 1622
1590 1623 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1591 1624 def initialize(self):
1592 1625 msvccompilerclass.initialize(self)
1593 1626 # "warning LNK4197: export 'func' specified multiple times"
1594 1627 self.ldflags_shared.append('/ignore:4197')
1595 1628 self.ldflags_shared_debug.append('/ignore:4197')
1596 1629
1597 1630 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1598 1631
1599 1632 packagedata = {
1600 1633 'mercurial': [
1601 1634 'locale/*/LC_MESSAGES/hg.mo',
1602 1635 'dummycert.pem',
1603 1636 ],
1604 1637 'mercurial.defaultrc': [
1605 1638 '*.rc',
1606 1639 ],
1607 1640 'mercurial.helptext': [
1608 1641 '*.txt',
1609 1642 ],
1610 1643 'mercurial.helptext.internals': [
1611 1644 '*.txt',
1612 1645 ],
1613 1646 'mercurial.thirdparty.attr': [
1614 1647 '*.pyi',
1615 1648 'py.typed',
1616 1649 ],
1617 1650 }
1618 1651
1619 1652
1620 1653 def ordinarypath(p):
1621 1654 return p and p[0] != '.' and p[-1] != '~'
1622 1655
1623 1656
1624 1657 for root in ('templates',):
1625 1658 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1626 1659 packagename = curdir.replace(os.sep, '.')
1627 1660 packagedata[packagename] = list(filter(ordinarypath, files))
1628 1661
1629 1662 datafiles = []
1630 1663
1631 1664 # distutils expects version to be str/unicode. Converting it to
1632 1665 # unicode on Python 2 still works because it won't contain any
1633 1666 # non-ascii bytes and will be implicitly converted back to bytes
1634 1667 # when operated on.
1635 1668 assert isinstance(version, str)
1636 1669 setupversion = version
1637 1670
1638 1671 extra = {}
1639 1672
1640 1673 py2exepackages = [
1641 1674 'hgdemandimport',
1642 1675 'hgext3rd',
1643 1676 'hgext',
1644 1677 'email',
1645 1678 # implicitly imported per module policy
1646 1679 # (cffi wouldn't be used as a frozen exe)
1647 1680 'mercurial.cext',
1648 1681 #'mercurial.cffi',
1649 1682 'mercurial.pure',
1650 1683 ]
1651 1684
1652 1685 py2exe_includes = []
1653 1686
1654 1687 py2exeexcludes = []
1655 1688 py2exedllexcludes = ['crypt32.dll']
1656 1689
1657 1690 if issetuptools:
1658 1691 extra['python_requires'] = supportedpy
1659 1692
1660 1693 if py2exeloaded:
1661 1694 extra['console'] = [
1662 1695 {
1663 1696 'script': 'hg',
1664 1697 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1665 1698 'product_version': version,
1666 1699 }
1667 1700 ]
1668 1701 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1669 1702 # Need to override hgbuild because it has a private copy of
1670 1703 # build.sub_commands.
1671 1704 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1672 1705 # put dlls in sub directory so that they won't pollute PATH
1673 1706 extra['zipfile'] = 'lib/library.zip'
1674 1707
1675 1708 # We allow some configuration to be supplemented via environment
1676 1709 # variables. This is better than setup.cfg files because it allows
1677 1710 # supplementing configs instead of replacing them.
1678 1711 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1679 1712 if extrapackages:
1680 1713 py2exepackages.extend(extrapackages.split(' '))
1681 1714
1682 1715 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1683 1716 if extra_includes:
1684 1717 py2exe_includes.extend(extra_includes.split(' '))
1685 1718
1686 1719 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1687 1720 if excludes:
1688 1721 py2exeexcludes.extend(excludes.split(' '))
1689 1722
1690 1723 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1691 1724 if dllexcludes:
1692 1725 py2exedllexcludes.extend(dllexcludes.split(' '))
1693 1726
1694 1727 if os.environ.get('PYOXIDIZER'):
1695 1728 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1696 1729
1697 1730 if os.name == 'nt':
1698 1731 # Windows binary file versions for exe/dll files must have the
1699 1732 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1700 1733 setupversion = setupversion.split(r'+', 1)[0]
1701 1734
1702 1735 setup(
1703 1736 name='mercurial',
1704 1737 version=setupversion,
1705 1738 author='Olivia Mackall and many others',
1706 1739 author_email='mercurial@mercurial-scm.org',
1707 1740 url='https://mercurial-scm.org/',
1708 1741 download_url='https://mercurial-scm.org/release/',
1709 1742 description=(
1710 1743 'Fast scalable distributed SCM (revision control, version '
1711 1744 'control) system'
1712 1745 ),
1713 1746 long_description=(
1714 1747 'Mercurial is a distributed SCM tool written in Python.'
1715 1748 ' It is used by a number of large projects that require'
1716 1749 ' fast, reliable distributed revision control, such as '
1717 1750 'Mozilla.'
1718 1751 ),
1719 1752 license='GNU GPLv2 or any later version',
1720 1753 classifiers=[
1721 1754 'Development Status :: 6 - Mature',
1722 1755 'Environment :: Console',
1723 1756 'Intended Audience :: Developers',
1724 1757 'Intended Audience :: System Administrators',
1725 1758 'License :: OSI Approved :: GNU General Public License (GPL)',
1726 1759 'Natural Language :: Danish',
1727 1760 'Natural Language :: English',
1728 1761 'Natural Language :: German',
1729 1762 'Natural Language :: Italian',
1730 1763 'Natural Language :: Japanese',
1731 1764 'Natural Language :: Portuguese (Brazilian)',
1732 1765 'Operating System :: Microsoft :: Windows',
1733 1766 'Operating System :: OS Independent',
1734 1767 'Operating System :: POSIX',
1735 1768 'Programming Language :: C',
1736 1769 'Programming Language :: Python',
1737 1770 'Topic :: Software Development :: Version Control',
1738 1771 ],
1739 1772 scripts=scripts,
1740 1773 packages=packages,
1741 1774 ext_modules=extmodules,
1742 1775 data_files=datafiles,
1743 1776 package_data=packagedata,
1744 1777 cmdclass=cmdclass,
1745 1778 distclass=hgdist,
1746 1779 options={
1747 1780 'py2exe': {
1748 1781 'bundle_files': 3,
1749 1782 'dll_excludes': py2exedllexcludes,
1750 1783 'includes': py2exe_includes,
1751 1784 'excludes': py2exeexcludes,
1752 1785 'packages': py2exepackages,
1753 1786 },
1754 1787 'bdist_mpkg': {
1755 1788 'zipdist': False,
1756 1789 'license': 'COPYING',
1757 1790 'readme': 'contrib/packaging/macosx/Readme.html',
1758 1791 'welcome': 'contrib/packaging/macosx/Welcome.html',
1759 1792 },
1760 1793 },
1761 1794 **extra
1762 1795 )
@@ -1,558 +1,555 b''
1 1 #require chg
2 2
3 3 Scale the timeout for the chg-server to the test timeout scaling.
4 4 This is done to reduce the flakiness of this test on heavy load.
5 5
6 6 $ CHGTIMEOUT=`expr $HGTEST_TIMEOUT / 6`
7 7 $ export CHGTIMEOUT
8 8
9 9 $ mkdir log
10 10 $ cp $HGRCPATH $HGRCPATH.unconfigured
11 11 $ cat <<'EOF' >> $HGRCPATH
12 12 > [cmdserver]
13 13 > log = $TESTTMP/log/server.log
14 14 > max-log-files = 1
15 15 > max-log-size = 10 kB
16 16 > EOF
17 17 $ cp $HGRCPATH $HGRCPATH.orig
18 18
19 19 $ filterlog () {
20 20 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
21 21 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
22 22 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
23 23 > -e 's!\(in \)[0-9.]*s\b!\1 ...s!g' \
24 24 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
25 25 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
26 26 > }
27 27
28 28 init repo
29 29
30 30 $ chg init foo
31 31 $ cd foo
32 32
33 33 ill-formed config
34 34
35 35 $ chg status
36 36 $ echo '=brokenconfig' >> $HGRCPATH
37 37 $ chg status
38 38 config error at * =brokenconfig (glob)
39 39 [30]
40 40
41 41 $ cp $HGRCPATH.orig $HGRCPATH
42 42
43 43 long socket path
44 44
45 45 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
46 46 $ mkdir -p $sockpath
47 47 $ bakchgsockname=$CHGSOCKNAME
48 48 $ CHGSOCKNAME=$sockpath/server
49 49 $ export CHGSOCKNAME
50 50 $ chg root
51 51 $TESTTMP/foo
52 52 $ rm -rf $sockpath
53 53 $ CHGSOCKNAME=$bakchgsockname
54 54 $ export CHGSOCKNAME
55 55
56 56 $ cd ..
57 57
58 58 editor
59 59 ------
60 60
61 61 $ cat >> pushbuffer.py <<EOF
62 62 > def reposetup(ui, repo):
63 63 > repo.ui.pushbuffer(subproc=True)
64 64 > EOF
65 65
66 66 $ chg init editor
67 67 $ cd editor
68 68
69 69 by default, system() should be redirected to the client:
70 70
71 71 $ touch foo
72 72 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
73 73 > | egrep "HG:|run 'cat"
74 74 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
75 75 HG: Enter commit message. Lines beginning with 'HG:' are removed.
76 76 HG: Leave message empty to abort commit.
77 77 HG: --
78 78 HG: user: test
79 79 HG: branch 'default'
80 80 HG: added foo
81 81
82 82 but no redirection should be made if output is captured:
83 83
84 84 $ touch bar
85 85 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
86 86 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
87 87 > | egrep "HG:|run 'cat"
88 88 [1]
89 89
90 90 check that commit commands succeeded:
91 91
92 92 $ hg log -T '{rev}:{desc}\n'
93 93 1:bufferred
94 94 0:channeled
95 95
96 96 $ cd ..
97 97
98 98 pager
99 99 -----
100 100
101 101 $ cat >> fakepager.py <<EOF
102 102 > import sys
103 103 > for line in sys.stdin:
104 104 > sys.stdout.write('paged! %r\n' % line)
105 105 > EOF
106 106
107 107 enable pager extension globally, but spawns the master server with no tty:
108 108
109 109 $ chg init pager
110 110 $ cd pager
111 111 $ cat >> $HGRCPATH <<EOF
112 112 > [extensions]
113 113 > pager =
114 114 > [pager]
115 115 > pager = "$PYTHON" $TESTTMP/fakepager.py
116 116 > EOF
117 117 $ chg version > /dev/null
118 118 $ touch foo
119 119 $ chg ci -qAm foo
120 120
121 121 pager should be enabled if the attached client has a tty:
122 122
123 123 $ chg log -l1 -q --config ui.formatted=True
124 124 paged! '0:1f7b0de80e11\n'
125 125 $ chg log -l1 -q --config ui.formatted=False
126 126 0:1f7b0de80e11
127 127
128 128 chg waits for pager if runcommand raises
129 129
130 130 $ cat > $TESTTMP/crash.py <<EOF
131 131 > from mercurial import registrar
132 132 > cmdtable = {}
133 133 > command = registrar.command(cmdtable)
134 134 > @command(b'crash')
135 135 > def pagercrash(ui, repo, *pats, **opts):
136 136 > ui.write(b'going to crash\n')
137 137 > raise Exception('.')
138 138 > EOF
139 139
140 140 $ cat > $TESTTMP/fakepager.py <<EOF
141 141 > import sys
142 142 > import time
143 143 > for line in iter(sys.stdin.readline, ''):
144 144 > if 'crash' in line: # only interested in lines containing 'crash'
145 145 > # if chg exits when pager is sleeping (incorrectly), the output
146 146 > # will be captured by the next test case
147 147 > time.sleep(1)
148 148 > sys.stdout.write('crash-pager: %s' % line)
149 149 > EOF
150 150
151 151 $ cat >> .hg/hgrc <<EOF
152 152 > [extensions]
153 153 > crash = $TESTTMP/crash.py
154 154 > EOF
155 155
156 156 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
157 157 crash-pager: going to crash
158 158 [255]
159 159
160 160 no stdout data should be printed after pager quits, and the buffered data
161 161 should never persist (issue6207)
162 162
163 163 "killed!" may be printed if terminated by SIGPIPE, which isn't important
164 164 in this test.
165 165
166 166 $ cat > $TESTTMP/bulkwrite.py <<'EOF'
167 167 > import time
168 168 > from mercurial import error, registrar
169 169 > cmdtable = {}
170 170 > command = registrar.command(cmdtable)
171 171 > @command(b'bulkwrite')
172 172 > def bulkwrite(ui, repo, *pats, **opts):
173 173 > ui.write(b'going to write massive data\n')
174 174 > ui.flush()
175 175 > t = time.time()
176 176 > while time.time() - t < 2:
177 177 > ui.write(b'x' * 1023 + b'\n') # will be interrupted by SIGPIPE
178 178 > raise error.Abort(b"write() doesn't block")
179 179 > EOF
180 180
181 181 $ cat > $TESTTMP/fakepager.py <<'EOF'
182 182 > import sys
183 183 > import time
184 184 > sys.stdout.write('paged! %r\n' % sys.stdin.readline())
185 185 > time.sleep(1) # new data will be written
186 186 > EOF
187 187
188 188 $ cat >> .hg/hgrc <<EOF
189 189 > [extensions]
190 190 > bulkwrite = $TESTTMP/bulkwrite.py
191 191 > EOF
192 192
193 193 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
194 194 paged! 'going to write massive data\n'
195 195 killed! (?)
196 196 [255]
197 197
198 198 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
199 199 paged! 'going to write massive data\n'
200 200 killed! (?)
201 201 [255]
202 202
203 203 $ cd ..
204 204
205 205 missing stdio
206 206 -------------
207 207
208 208 $ CHGDEBUG=1 chg version -q 0<&-
209 209 chg: debug: * stdio fds are missing (glob)
210 210 chg: debug: * execute original hg (glob)
211 211 Mercurial Distributed SCM * (glob)
212 212
213 213 server lifecycle
214 214 ----------------
215 215
216 216 chg server should be restarted on code change, and old server will shut down
217 217 automatically. In this test, we use the following time parameters:
218 218
219 219 - "sleep 1" to make mtime different
220 220 - "sleep 2" to notice mtime change (polling interval is 1 sec)
221 221
222 222 set up repository with an extension:
223 223
224 224 $ chg init extreload
225 225 $ cd extreload
226 226 $ touch dummyext.py
227 227 $ cat <<EOF >> .hg/hgrc
228 228 > [extensions]
229 229 > dummyext = dummyext.py
230 230 > EOF
231 231
232 232 isolate socket directory for stable result:
233 233
234 234 $ OLDCHGSOCKNAME=$CHGSOCKNAME
235 235 $ mkdir chgsock
236 236 $ CHGSOCKNAME=`pwd`/chgsock/server
237 237
238 238 warm up server:
239 239
240 240 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
241 241 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
242 242
243 243 new server should be started if extension modified:
244 244
245 245 $ sleep 1
246 246 $ touch dummyext.py
247 247 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
248 248 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
249 249 chg: debug: * instruction: reconnect (glob)
250 250 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
251 251
252 252 old server will shut down, while new server should still be reachable:
253 253
254 254 $ sleep 2
255 255 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
256 256
257 257 socket file should never be unlinked by old server:
258 258 (simulates unowned socket by updating mtime, which makes sure server exits
259 259 at polling cycle)
260 260
261 261 $ ls chgsock/server-*
262 262 chgsock/server-* (glob)
263 263 $ touch chgsock/server-*
264 264 $ sleep 2
265 265 $ ls chgsock/server-*
266 266 chgsock/server-* (glob)
267 267
268 268 since no server is reachable from socket file, new server should be started:
269 269 (this test makes sure that old server shut down automatically)
270 270
271 271 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
272 272 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
273 273
274 274 shut down servers and restore environment:
275 275
276 276 $ rm -R chgsock
277 277 $ sleep 2
278 278 $ CHGSOCKNAME=$OLDCHGSOCKNAME
279 279 $ cd ..
280 280
281 281 check that server events are recorded:
282 282
283 283 $ ls log
284 284 server.log
285 285 server.log.1
286 286
287 287 print only the last 10 lines, since we aren't sure how many records are
288 288 preserved (since setprocname isn't available on py3 and pure version,
289 289 the 10th-most-recent line is different when using py3):
290 290
291 291 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
292 292 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (no-setprocname !)
293 293 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
294 294 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (setprocname !)
295 295 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
296 296 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
297 297 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
298 298 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
299 299 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
300 300 YYYY/MM/DD HH:MM:SS (PID)> validate: []
301 301 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
302 302 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
303 303
304 304 global data mutated by schems
305 305 -----------------------------
306 306
307 307 $ hg init schemes
308 308 $ cd schemes
309 309
310 310 initial state
311 311
312 312 $ cat > .hg/hgrc <<'EOF'
313 313 > [extensions]
314 314 > schemes =
315 315 > [schemes]
316 316 > foo = https://foo.example.org/
317 317 > EOF
318 318 $ hg debugexpandscheme foo://expanded
319 319 https://foo.example.org/expanded
320 320 $ hg debugexpandscheme bar://unexpanded
321 321 bar://unexpanded
322 322
323 323 add bar
324 324
325 325 $ cat > .hg/hgrc <<'EOF'
326 326 > [extensions]
327 327 > schemes =
328 328 > [schemes]
329 329 > foo = https://foo.example.org/
330 330 > bar = https://bar.example.org/
331 331 > EOF
332 332 $ hg debugexpandscheme foo://expanded
333 333 https://foo.example.org/expanded
334 334 $ hg debugexpandscheme bar://expanded
335 335 https://bar.example.org/expanded
336 336
337 337 remove foo
338 338
339 339 $ cat > .hg/hgrc <<'EOF'
340 340 > [extensions]
341 341 > schemes =
342 342 > [schemes]
343 343 > bar = https://bar.example.org/
344 344 > EOF
345 345 $ hg debugexpandscheme foo://unexpanded
346 346 foo://unexpanded
347 347 $ hg debugexpandscheme bar://expanded
348 348 https://bar.example.org/expanded
349 349
350 350 $ cd ..
351 351
352 352 repository cache
353 353 ----------------
354 354
355 $ rm log/server.log*
356 355 $ cp $HGRCPATH.unconfigured $HGRCPATH
357 356 $ cat <<'EOF' >> $HGRCPATH
358 357 > [cmdserver]
359 > log = $TESTTMP/log/server.log
358 > log = $TESTTMP/log/server-cached.log
360 359 > max-repo-cache = 1
361 360 > track-log = command, repocache
362 361 > EOF
363 362
364 363 isolate socket directory for stable result:
365 364
366 365 $ OLDCHGSOCKNAME=$CHGSOCKNAME
367 366 $ mkdir chgsock
368 367 $ CHGSOCKNAME=`pwd`/chgsock/server
369 368
370 369 create empty repo and cache it:
371 370
372 371 $ hg init cached
373 372 $ hg id -R cached
374 373 000000000000 tip
375 374 $ sleep 1
376 375
377 376 modify repo (and cache will be invalidated):
378 377
379 378 $ touch cached/a
380 379 $ hg ci -R cached -Am 'add a'
381 380 adding a
382 381 $ sleep 1
383 382
384 383 read cached repo:
385 384
386 385 $ hg log -R cached
387 386 changeset: 0:ac82d8b1f7c4
388 387 tag: tip
389 388 user: test
390 389 date: Thu Jan 01 00:00:00 1970 +0000
391 390 summary: add a
392 391
393 392 $ sleep 1
394 393
395 394 discard cached from LRU cache:
396 395
397 396 $ hg clone cached cached2
398 397 updating to branch default
399 398 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
400 399 $ hg id -R cached2
401 400 ac82d8b1f7c4 tip
402 401 $ sleep 1
403 402
404 403 read uncached repo:
405 404
406 405 $ hg log -R cached
407 406 changeset: 0:ac82d8b1f7c4
408 407 tag: tip
409 408 user: test
410 409 date: Thu Jan 01 00:00:00 1970 +0000
411 410 summary: add a
412 411
413 412 $ sleep 1
414 413
415 414 shut down servers and restore environment:
416 415
417 416 $ rm -R chgsock
418 417 $ sleep 2
419 418 $ CHGSOCKNAME=$OLDCHGSOCKNAME
420 419
421 420 check server log:
422 421
423 $ cat log/server.log | filterlog
424 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
425 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...) (?)
422 $ cat log/server-cached.log | filterlog
426 423 YYYY/MM/DD HH:MM:SS (PID)> init cached
427 424 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
428 425 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
429 426 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
430 427 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
431 428 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
432 429 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
433 430 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
434 431 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
435 432 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
436 433 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
437 434 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
438 435 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
439 436 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
440 437
441 438 Test that -R is interpreted relative to --cwd.
442 439
443 440 $ hg init repo1
444 441 $ mkdir -p a/b
445 442 $ hg init a/b/repo2
446 443 $ printf "[alias]\ntest=repo1\n" >> repo1/.hg/hgrc
447 444 $ printf "[alias]\ntest=repo2\n" >> a/b/repo2/.hg/hgrc
448 445 $ cd a
449 446 $ chg --cwd .. -R repo1 show alias.test
450 447 repo1
451 448 $ chg --cwd . -R b/repo2 show alias.test
452 449 repo2
453 450 $ cd ..
454 451
455 452 Test that chg works (sets to the user's actual LC_CTYPE) even when python
456 453 "coerces" the locale (py3.7+)
457 454
458 455 $ cat > $TESTTMP/debugenv.py <<EOF
459 456 > from mercurial import encoding
460 457 > from mercurial import registrar
461 458 > cmdtable = {}
462 459 > command = registrar.command(cmdtable)
463 460 > @command(b'debugenv', [], b'', norepo=True)
464 461 > def debugenv(ui):
465 462 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
466 463 > v = encoding.environ.get(k)
467 464 > if v is not None:
468 465 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
469 466 > EOF
470 467 (hg keeps python's modified LC_CTYPE, chg doesn't)
471 468 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
472 469 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
473 470 LC_CTYPE=C.UTF-8 (py37 !)
474 471 LC_CTYPE= (no-py37 !)
475 472 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
476 473 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
477 474 LC_CTYPE=
478 475 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
479 476 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
480 477 *cannot change locale* (glob) (?)
481 478 LC_CTYPE=unsupported_value
482 479 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
483 480 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
484 481 LC_CTYPE=
485 482 $ LANG= LC_ALL= LC_CTYPE= chg \
486 483 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
487 484 LC_ALL=
488 485 LC_CTYPE=
489 486 LANG=
490 487
491 488 Profiling isn't permanently enabled or carried over between chg invocations that
492 489 share the same server
493 490 $ cp $HGRCPATH.orig $HGRCPATH
494 491 $ hg init $TESTTMP/profiling
495 492 $ cd $TESTTMP/profiling
496 493 $ filteredchg() {
497 494 > CHGDEBUG=1 chg "$@" 2>&1 | sed -rn 's_^No samples recorded.*$_Sample count: 0_; /Sample count/p; /start cmdserver/p'
498 495 > }
499 496 $ newchg() {
500 497 > chg --kill-chg-daemon
501 498 > filteredchg "$@" | egrep -v 'start cmdserver' || true
502 499 > }
503 500 (--profile isn't permanently on just because it was specified when chg was
504 501 started)
505 502 $ newchg log -r . --profile
506 503 Sample count: * (glob)
507 504 $ filteredchg log -r .
508 505 (enabling profiling via config works, even on the first chg command that starts
509 506 a cmdserver)
510 507 $ cat >> $HGRCPATH <<EOF
511 508 > [profiling]
512 509 > type=stat
513 510 > enabled=1
514 511 > EOF
515 512 $ newchg log -r .
516 513 Sample count: * (glob)
517 514 $ filteredchg log -r .
518 515 Sample count: * (glob)
519 516 (test that we aren't accumulating more and more samples each run)
520 517 $ cat > $TESTTMP/debugsleep.py <<EOF
521 518 > import time
522 519 > from mercurial import registrar
523 520 > cmdtable = {}
524 521 > command = registrar.command(cmdtable)
525 522 > @command(b'debugsleep', [], b'', norepo=True)
526 523 > def debugsleep(ui):
527 524 > start = time.time()
528 525 > x = 0
529 526 > while time.time() < start + 0.5:
530 527 > time.sleep(.1)
531 528 > x += 1
532 529 > ui.status(b'%d debugsleep iterations in %.03fs\n' % (x, time.time() - start))
533 530 > EOF
534 531 $ cat >> $HGRCPATH <<EOF
535 532 > [extensions]
536 533 > debugsleep = $TESTTMP/debugsleep.py
537 534 > EOF
538 535 $ newchg debugsleep > run_1
539 536 $ filteredchg debugsleep > run_2
540 537 $ filteredchg debugsleep > run_3
541 538 $ filteredchg debugsleep > run_4
542 539 FIXME: Run 4 should not be >3x Run 1's number of samples.
543 540 $ "$PYTHON" <<EOF
544 541 > r1 = int(open("run_1", "r").read().split()[-1])
545 542 > r4 = int(open("run_4", "r").read().split()[-1])
546 543 > print("Run 1: %d samples\nRun 4: %d samples\nRun 4 > 3 * Run 1: %s" %
547 544 > (r1, r4, r4 > (r1 * 3)))
548 545 > EOF
549 546 Run 1: * samples (glob)
550 547 Run 4: * samples (glob)
551 548 Run 4 > 3 * Run 1: False
552 549 (Disabling with --no-profile on the commandline still works, but isn't permanent)
553 550 $ newchg log -r . --no-profile
554 551 $ filteredchg log -r .
555 552 Sample count: * (glob)
556 553 $ filteredchg log -r . --no-profile
557 554 $ filteredchg log -r .
558 555 Sample count: * (glob)
General Comments 0
You need to be logged in to leave comments. Login now