doc: Prefer https urls where possible.
[gnulib.git] / pygnulib / constants.py
blob8c4671116b73008ba153ffe9e260cf18b3b5a5f0
1 # Copyright (C) 2002-2024 Free Software Foundation, Inc.
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <https://www.gnu.org/licenses/>.
16 '''An easy access to pygnulib constants.'''
18 from __future__ import unicode_literals
19 from __future__ import annotations
21 #===============================================================================
22 # Define global imports
23 #===============================================================================
24 import re
25 import os
26 import sys
27 import stat
28 import shutil
29 import tempfile
30 import subprocess as sp
31 import __main__ as interpreter
33 #===============================================================================
34 # Define module information
35 #===============================================================================
36 __all__ = []
39 #===============================================================================
40 # Define global constants
41 #===============================================================================
42 # Declare necessary variables
43 APP = dict() # Application
44 DIRS = dict() # Directories
45 UTILS = dict() # Utilities
46 ENCS = dict() # Encodings
47 MODES = dict() # Modes
48 TESTS = dict() # Tests
50 # Set ENCS dictionary
51 if not hasattr(interpreter, '__file__'):
52 if sys.stdout.encoding != None:
53 ENCS['default'] = sys.stdout.encoding
54 else: # sys.stdout.encoding == None
55 ENCS['default'] = 'UTF-8'
56 else: # if hasattr(interpreter, '__file__'):
57 ENCS['default'] = 'UTF-8'
58 ENCS['system'] = sys.getfilesystemencoding()
59 ENCS['shell'] = sys.stdout.encoding
60 if ENCS['shell'] == None:
61 ENCS['shell'] = 'UTF-8'
63 # Set APP dictionary
64 APP['path'] = os.path.realpath(sys.argv[0]) # file name of <gnulib>/.gnulib-tool.py
65 APP['root'] = os.path.dirname(APP['path']) # file name of <gnulib>
66 APP['name'] = os.path.join(APP['root'], 'gnulib-tool.py')
68 # Set DIRS directory
69 DIRS['cwd'] = os.getcwd()
70 def init_DIRS(gnulib_dir: str) -> None:
71 DIRS['root'] = gnulib_dir
72 DIRS['build-aux'] = os.path.join(gnulib_dir, 'build-aux')
73 DIRS['config'] = os.path.join(gnulib_dir, 'config')
74 DIRS['doc'] = os.path.join(gnulib_dir, 'doc')
75 DIRS['lib'] = os.path.join(gnulib_dir, 'lib')
76 DIRS['m4'] = os.path.join(gnulib_dir, 'm4')
77 DIRS['modules'] = os.path.join(gnulib_dir, 'modules')
78 DIRS['tests'] = os.path.join(gnulib_dir, 'tests')
79 DIRS['git'] = os.path.join(gnulib_dir, '.git')
80 DIRS['cvs'] = os.path.join(gnulib_dir, 'CVS')
82 # Set MODES dictionary
83 MODES = \
85 'import': 0,
86 'add-import': 1,
87 'remove-import': 2,
88 'update': 3,
90 MODES['verbose-min'] = -2
91 MODES['verbose-default'] = 0
92 MODES['verbose-max'] = 2
94 # Define TESTS categories
95 TESTS = \
97 'tests': 0,
98 'c++-test': 1,
99 'cxx-test': 1,
100 'c++-tests': 1,
101 'cxx-tests': 1,
102 'longrunning-test': 2,
103 'longrunning-tests': 2,
104 'privileged-test': 3,
105 'privileged-tests': 3,
106 'unportable-test': 4,
107 'unportable-tests': 4,
108 'all-test': 5,
109 'all-tests': 5,
112 # Define AUTOCONF minimum version
113 DEFAULT_AUTOCONF_MINVERSION = 2.64
114 # You can set AUTOCONFPATH to empty if autoconf ≥ 2.64 is already in your PATH
115 AUTOCONFPATH = ''
116 # You can set AUTOMAKEPATH to empty if automake ≥ 1.14 is already in your PATH
117 AUTOMAKEPATH = ''
118 # You can set GETTEXTPATH to empty if autopoint ≥ 0.15 is already in your PATH
119 GETTEXTPATH = ''
120 # You can set LIBTOOLPATH to empty if libtoolize 2.x is already in your PATH
121 LIBTOOLPATH = ''
123 # You can also set the variable AUTOCONF individually
124 if AUTOCONFPATH:
125 UTILS['autoconf'] = AUTOCONFPATH + 'autoconf'
126 else:
127 if os.getenv('AUTOCONF'):
128 UTILS['autoconf'] = os.getenv('AUTOCONF')
129 else:
130 UTILS['autoconf'] = 'autoconf'
132 # You can also set the variable AUTORECONF individually
133 if AUTOCONFPATH:
134 UTILS['autoreconf'] = AUTOCONFPATH + 'autoreconf'
135 else:
136 if os.getenv('AUTORECONF'):
137 UTILS['autoreconf'] = os.getenv('AUTORECONF')
138 else:
139 UTILS['autoreconf'] = 'autoreconf'
141 # You can also set the variable AUTOHEADER individually
142 if AUTOCONFPATH:
143 UTILS['autoheader'] = AUTOCONFPATH + 'autoheader'
144 else:
145 if os.getenv('AUTOHEADER'):
146 UTILS['autoheader'] = os.getenv('AUTOHEADER')
147 else:
148 UTILS['autoheader'] = 'autoheader'
150 # You can also set the variable AUTOMAKE individually
151 if AUTOMAKEPATH:
152 UTILS['automake'] = AUTOMAKEPATH + 'automake'
153 else:
154 if os.getenv('AUTOMAKE'):
155 UTILS['automake'] = os.getenv('AUTOMAKE')
156 else:
157 UTILS['automake'] = 'automake'
159 # You can also set the variable ACLOCAL individually
160 if AUTOMAKEPATH:
161 UTILS['aclocal'] = AUTOMAKEPATH + 'aclocal'
162 else:
163 if os.getenv('ACLOCAL'):
164 UTILS['aclocal'] = os.getenv('ACLOCAL')
165 else:
166 UTILS['aclocal'] = 'aclocal'
168 # You can also set the variable AUTOPOINT individually
169 if GETTEXTPATH:
170 UTILS['autopoint'] = GETTEXTPATH + 'autopoint'
171 else:
172 if os.getenv('AUTOPOINT'):
173 UTILS['autopoint'] = os.getenv('AUTOPOINT')
174 else:
175 UTILS['autopoint'] = 'autopoint'
177 # You can also set the variable LIBTOOLIZE individually
178 if LIBTOOLPATH:
179 UTILS['libtoolize'] = LIBTOOLPATH + 'libtoolize'
180 else:
181 if os.getenv('LIBTOOLIZE'):
182 UTILS['libtoolize'] = os.getenv('LIBTOOLIZE')
183 else:
184 UTILS['libtoolize'] = 'libtoolize'
186 # You can also set the variable MAKE
187 if os.getenv('MAKE'):
188 UTILS['make'] = os.getenv('MAKE')
189 else:
190 UTILS['make'] = 'make'
193 #===============================================================================
194 # Define global functions
195 #===============================================================================
197 def force_output() -> None:
198 '''This function is to be invoked before invoking external programs.
199 It initiates bringing the the contents of process-internal output buffers
200 to their respective destinations.'''
201 sys.stdout.flush()
202 sys.stderr.flush()
205 def execute(args: list[str], verbose: int) -> None:
206 '''Execute the given shell command.'''
207 if verbose >= 0:
208 print('executing %s' % ' '.join(args), flush=True)
209 try: # Try to run
210 retcode = sp.call(args)
211 except Exception as error:
212 sys.stderr.write(str(error) + '\n')
213 sys.exit(1)
214 else:
215 # Commands like automake produce output to stderr even when they succeed.
216 # Turn this output off if the command succeeds.
217 temp = tempfile.mktemp()
218 xargs = '%s > %s 2>&1' % (' '.join(args), temp)
219 try: # Try to run
220 retcode = sp.call(xargs, shell=True)
221 except Exception as error:
222 sys.stderr.write(str(error) + '\n')
223 sys.exit(1)
224 if retcode == 0:
225 os.remove(temp)
226 else:
227 print('executing %s' % ' '.join(args))
228 with open(temp, mode='r', newline='\n', encoding='utf-8') as file:
229 cmdout = file.read()
230 print(cmdout)
231 os.remove(temp)
232 sys.exit(retcode)
235 def cleaner(sequence: str | list[str]) -> str | list[str | bool]:
236 '''Clean string or list of strings after using regex.'''
237 if type(sequence) is str:
238 sequence = sequence.replace('[', '')
239 sequence = sequence.replace(']', '')
240 elif type(sequence) is list:
241 sequence = [ value.replace('[', '').replace(']', '')
242 for value in sequence]
243 sequence = [ value.replace('(', '').replace(')', '')
244 for value in sequence]
245 sequence = [ False if value == 'false' else value
246 for value in sequence ]
247 sequence = [ True if value == 'true' else value
248 for value in sequence ]
249 sequence = [ value.strip()
250 if type(value) is str else value
251 for value in sequence ]
252 return sequence
255 def joinpath(head: str, *tail: str) -> str:
256 '''Join two or more pathname components, inserting '/' as needed. If any
257 component is an absolute path, all previous path components will be
258 discarded.
259 This function also replaces SUBDIR/../ with empty; therefore it is not
260 suitable when some of the pathname components use Makefile variables
261 such as '$(srcdir)'.'''
262 return os.path.normpath(os.path.join(head, *tail))
265 def relativize(dir1: str, dir2: str) -> str:
266 '''Compute a relative pathname reldir such that dir1/reldir = dir2.
267 dir1 and dir2 must be relative pathnames.'''
268 dir0 = os.getcwd()
269 while dir1:
270 dir1 = '%s%s' % (os.path.normpath(dir1), os.path.sep)
271 dir2 = '%s%s' % (os.path.normpath(dir2), os.path.sep)
272 first = dir1[:dir1.find(os.path.sep)]
273 if first != '.':
274 if first == '..':
275 dir2 = joinpath(os.path.basename(dir0), dir2)
276 dir0 = os.path.dirname(dir0)
277 else: # if first != '..'
278 # Get first component of dir2
279 first2 = dir2[:dir2.find(os.path.sep)]
280 if first == first2:
281 dir2 = dir2[dir2.find(os.path.sep) + 1:]
282 else: # if first != first2
283 dir2 = joinpath('..', dir2)
284 dir0 = joinpath(dir0, first)
285 dir1 = dir1[dir1.find(os.path.sep) + 1:]
286 result = os.path.normpath(dir2)
287 return result
290 def relconcat(dir1: str, dir2: str) -> str:
291 '''Compute a relative pathname dir1/dir2, with obvious simplifications.
292 dir1 and dir2 must be relative pathnames.
293 dir2 is considered to be relative to dir1.'''
294 return os.path.normpath(os.path.join(dir1, dir2))
297 def ensure_writable(dest: str) -> None:
298 '''Ensure that the file dest is writable.'''
299 # os.stat throws FileNotFoundError error but we assume it exists.
300 st = os.stat(dest)
301 if not (st.st_mode & stat.S_IWUSR):
302 os.chmod(dest, st.st_mode | stat.S_IWUSR)
305 def relinverse(dir: str) -> str:
306 '''Compute the inverse of dir. Namely, a relative pathname consisting only
307 of '..' components, such that dir/relinverse = '.'.
308 dir must be a relative pathname.'''
309 if False:
310 # This should work too.
311 return relativize(dir, '.')
312 else:
313 inverse = ''
314 for component in dir.split('/'):
315 if component != '':
316 inverse += '../'
317 return os.path.normpath(inverse)
320 def copyfile(src: str, dest: str) -> None:
321 '''Copy file src to file dest. Like shutil.copy, but ignore errors e.g. on
322 VFAT file systems.'''
323 shutil.copyfile(src, dest)
324 try:
325 shutil.copymode(src, dest)
326 except PermissionError:
327 pass
330 def copyfile2(src: str, dest: str) -> None:
331 '''Copy file src to file dest, preserving modification time. Like
332 shutil.copy2, but ignore errors e.g. on VFAT file systems. This function
333 is to be used for backup files.'''
334 shutil.copyfile(src, dest)
335 try:
336 shutil.copystat(src, dest)
337 except PermissionError:
338 pass
341 def movefile(src: str, dest: str) -> None:
342 '''Move/rename file src to file dest. Like shutil.move, but gracefully
343 handle common errors.'''
344 try:
345 shutil.move(src, dest)
346 except PermissionError:
347 # shutil.move invokes os.rename, catches the resulting OSError for
348 # errno=EXDEV, attempts a copy instead, and encounters a PermissionError
349 # while doing that.
350 copyfile2(src, dest)
351 os.remove(src)
354 def symlink_relative(src: str, dest: str) -> None:
355 '''Like ln -s, except use cp -p if ln -s fails.
356 src is either absolute or relative to the directory of dest.'''
357 try:
358 os.symlink(src, dest)
359 except PermissionError:
360 sys.stderr.write('%s: ln -s failed; falling back on cp -p\n' % APP['name'])
361 if os.path.isabs(src):
362 # src is absolute.
363 cp_src = src
364 else:
365 # src is relative to the directory of dest.
366 last_slash = dest.rfind('/')
367 if last_slash >= 0:
368 cp_src = joinpath(dest[0:last_slash-1], src)
369 else:
370 cp_src = src
371 copyfile2(cp_src, dest)
372 ensure_writable(dest)
375 def as_link_value_at_dest(src: str, dest: str) -> str:
376 '''Compute the symbolic link value to place at dest, such that the
377 resulting symbolic link points to src. src is given relative to the
378 current directory (or absolute).'''
379 if type(src) is not str:
380 raise TypeError('src must be a string, not %s' % (type(src).__name__))
381 if type(dest) is not str:
382 raise TypeError('dest must be a string, not %s' % (type(dest).__name__))
383 if os.path.isabs(src):
384 return src
385 else: # if src is not absolute
386 if os.path.isabs(dest):
387 return joinpath(os.getcwd(), src)
388 else: # if dest is not absolute
389 destdir = os.path.dirname(dest)
390 if not destdir:
391 destdir = '.'
392 return relativize(destdir, src)
395 def link_relative(src: str, dest: str) -> None:
396 '''Like ln -s, except that src is given relative to the current directory
397 (or absolute), not given relative to the directory of dest.'''
398 if type(src) is not str:
399 raise TypeError('src must be a string, not %s' % (type(src).__name__))
400 if type(dest) is not str:
401 raise TypeError('dest must be a string, not %s' % (type(dest).__name__))
402 link_value = as_link_value_at_dest(src, dest)
403 symlink_relative(link_value, dest)
406 def link_if_changed(src: str, dest: str) -> None:
407 '''Create a symlink, but avoids munging timestamps if the link is correct.'''
408 link_value = as_link_value_at_dest(src, dest)
409 if not (os.path.islink(dest) and os.readlink(dest) == link_value):
410 try:
411 os.remove(dest)
412 except FileNotFoundError:
413 pass
414 # Equivalent to link_relative(src, dest):
415 symlink_relative(link_value, dest)
418 def hardlink(src: str, dest: str) -> None:
419 '''Like ln, except use cp -p if ln fails.
420 src is either absolute or relative to the directory of dest.'''
421 try:
422 os.link(src, dest)
423 except PermissionError:
424 sys.stderr.write('%s: ln failed; falling back on cp -p\n' % APP['name'])
425 if os.path.isabs(src):
426 # src is absolute.
427 cp_src = src
428 else:
429 # src is relative to the directory of dest.
430 last_slash = dest.rfind('/')
431 if last_slash >= 0:
432 cp_src = joinpath(dest[0: last_slash - 1], src)
433 else:
434 cp_src = src
435 copyfile2(cp_src, dest)
436 ensure_writable(dest)
439 def rmtree(dest: str) -> None:
440 '''Removes the file or directory tree at dest, if it exists.'''
441 # These two implementations are nearly equivalent.
442 # Speed: 'rm -rf' can be a little faster.
443 # Exceptions: shutil.rmtree raises Python exceptions, e.g. PermissionError.
444 if True:
445 sp.run(['rm', '-rf', dest], shell=False)
446 else:
447 try:
448 shutil.rmtree(dest)
449 except FileNotFoundError:
450 pass
453 def filter_filelist(separator: str, filelist: list[str], prefix: str, suffix: str,
454 removed_prefix: str, removed_suffix: str,
455 added_prefix: str = '', added_suffix: str = '') -> str:
456 '''Filter the given list of files. Filtering: Only the elements starting with
457 prefix and ending with suffix are considered. Processing: removed_prefix
458 and removed_suffix are removed from each element, added_prefix and
459 added_suffix are added to each element.'''
460 listing = []
461 for filename in filelist:
462 if filename.startswith(prefix) and filename.endswith(suffix):
463 pattern = re.compile(r'^%s(.*)%s$'
464 % (removed_prefix, removed_suffix))
465 result = pattern.sub(r'%s\1%s'
466 % (added_prefix, added_suffix), filename)
467 listing.append(result)
468 # Return an empty string if no files were matched, else combine them
469 # with the given separator.
470 if listing:
471 result = separator.join(listing)
472 else:
473 result = ''
474 return result
477 def lines_to_multiline(lines: list[str]) -> str:
478 '''Combine the lines to a single string, terminating each line with a
479 newline character.'''
480 if len(lines) > 0:
481 return '\n'.join(lines) + '\n'
482 else:
483 return ''
486 def substart(orig: str, repl: str, data: str) -> str:
487 '''Replaces the start portion of a string.
489 Returns data with orig replaced by repl, but only at the beginning of data.
490 Like data.replace(orig,repl), except only at the beginning of data.'''
491 result = data
492 if data.startswith(orig):
493 result = repl + data[len(orig):]
494 return result
497 def subend(orig: str, repl: str, data: str) -> str:
498 '''Replaces the end portion of a string.
500 Returns data with orig replaced by repl, but only at the end of data.
501 Like data.replace(orig,repl), except only at the end of data.'''
502 result = data
503 if data.endswith(orig):
504 result = data[:-len(orig)] + repl
505 return result
508 def remove_trailing_slashes(text: str) -> str:
509 '''Remove trailing slashes from a file name, except when the file name
510 consists only of slashes.'''
511 result = text
512 while result.endswith('/'):
513 result = result[:-1]
514 if result == '':
515 result = text
516 break
517 return result
520 def combine_lines(text: str) -> str:
521 '''Given a multiline string text, join lines by spaces:
522 When a line ends in a backslash, remove the backslash and join the next
523 line to it, inserting a space between them.'''
524 return text.replace('\\\n', ' ')
527 def combine_lines_matching(pattern: re.Pattern, text: str) -> str:
528 '''Given a multiline string text, join lines by spaces, when the first
529 such line matches a given RegexObject pattern.
530 When a line that matches the pattern ends in a backslash, remove the
531 backslash and join the next line to it, inserting a space between them.
532 When a line that is the result of such a join ends in a backslash,
533 proceed likewise.'''
534 outerpos = 0
535 match = pattern.search(text, outerpos)
536 while match:
537 (startpos, pos) = match.span()
538 # Look how far the continuation lines extend.
539 pos = text.find('\n', pos)
540 while pos > 0 and text[pos - 1] == '\\':
541 pos = text.find('\n', pos + 1)
542 if pos < 0:
543 pos = len(text)
544 # Perform a combine_lines throughout the continuation lines.
545 partdone = text[:startpos] + combine_lines(text[startpos:pos])
546 outerpos = len(partdone)
547 text = partdone + text[pos:]
548 # Next round.
549 match = pattern.search(text, outerpos)
550 return text
553 def get_terminfo_string(capability: str) -> str:
554 '''Returns the value of a string-type terminfo capability for the current value of $TERM.
555 Returns the empty string if not defined.'''
556 value = ''
557 try:
558 value = sp.run(['tput', capability], stdout=sp.PIPE, stderr=sp.DEVNULL).stdout.decode('utf-8')
559 except Exception:
560 pass
561 return value
564 def bold_escapes() -> tuple[str, str]:
565 '''Returns the escape sequences for turning bold-face on and off.'''
566 term = os.getenv('TERM', '')
567 if term != '' and os.isatty(1):
568 if term.startswith('xterm'):
569 # Assume xterm compatible escape sequences.
570 bold_on = '\033[1m'
571 bold_off = '\033[0m'
572 else:
573 # Use the terminfo capability strings for "bold" and "sgr0".
574 if term == 'sun-color' and get_terminfo_string('smso') != get_terminfo_string('rev'):
575 # Solaris 11 OmniOS: `tput smso` renders as bold,
576 # `tput rmso` is the same as `tput sgr0`.
577 bold_on = get_terminfo_string('smso')
578 bold_off = get_terminfo_string('rmso')
579 else:
580 bold_on = get_terminfo_string('bold')
581 bold_off = get_terminfo_string('sgr0')
582 if bold_on == '' or bold_off == '':
583 bold_on = ''
584 bold_off = ''
585 else:
586 bold_on = ''
587 bold_off = ''
588 return (bold_on, bold_off)
591 __all__ += ['APP', 'DIRS', 'MODES', 'UTILS']