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 #===============================================================================
30 import subprocess
as sp
31 import __main__
as interpreter
33 #===============================================================================
34 # Define module information
35 #===============================================================================
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
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'
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')
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
90 MODES
['verbose-min'] = -2
91 MODES
['verbose-default'] = 0
92 MODES
['verbose-max'] = 2
94 # Define TESTS categories
102 'longrunning-test': 2,
103 'longrunning-tests': 2,
104 'privileged-test': 3,
105 'privileged-tests': 3,
106 'unportable-test': 4,
107 'unportable-tests': 4,
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
116 # You can set AUTOMAKEPATH to empty if automake ≥ 1.14 is already in your PATH
118 # You can set GETTEXTPATH to empty if autopoint ≥ 0.15 is already in your PATH
120 # You can set LIBTOOLPATH to empty if libtoolize 2.x is already in your PATH
123 # You can also set the variable AUTOCONF individually
125 UTILS
['autoconf'] = AUTOCONFPATH
+ 'autoconf'
127 if os
.getenv('AUTOCONF'):
128 UTILS
['autoconf'] = os
.getenv('AUTOCONF')
130 UTILS
['autoconf'] = 'autoconf'
132 # You can also set the variable AUTORECONF individually
134 UTILS
['autoreconf'] = AUTOCONFPATH
+ 'autoreconf'
136 if os
.getenv('AUTORECONF'):
137 UTILS
['autoreconf'] = os
.getenv('AUTORECONF')
139 UTILS
['autoreconf'] = 'autoreconf'
141 # You can also set the variable AUTOHEADER individually
143 UTILS
['autoheader'] = AUTOCONFPATH
+ 'autoheader'
145 if os
.getenv('AUTOHEADER'):
146 UTILS
['autoheader'] = os
.getenv('AUTOHEADER')
148 UTILS
['autoheader'] = 'autoheader'
150 # You can also set the variable AUTOMAKE individually
152 UTILS
['automake'] = AUTOMAKEPATH
+ 'automake'
154 if os
.getenv('AUTOMAKE'):
155 UTILS
['automake'] = os
.getenv('AUTOMAKE')
157 UTILS
['automake'] = 'automake'
159 # You can also set the variable ACLOCAL individually
161 UTILS
['aclocal'] = AUTOMAKEPATH
+ 'aclocal'
163 if os
.getenv('ACLOCAL'):
164 UTILS
['aclocal'] = os
.getenv('ACLOCAL')
166 UTILS
['aclocal'] = 'aclocal'
168 # You can also set the variable AUTOPOINT individually
170 UTILS
['autopoint'] = GETTEXTPATH
+ 'autopoint'
172 if os
.getenv('AUTOPOINT'):
173 UTILS
['autopoint'] = os
.getenv('AUTOPOINT')
175 UTILS
['autopoint'] = 'autopoint'
177 # You can also set the variable LIBTOOLIZE individually
179 UTILS
['libtoolize'] = LIBTOOLPATH
+ 'libtoolize'
181 if os
.getenv('LIBTOOLIZE'):
182 UTILS
['libtoolize'] = os
.getenv('LIBTOOLIZE')
184 UTILS
['libtoolize'] = 'libtoolize'
186 # You can also set the variable MAKE
187 if os
.getenv('MAKE'):
188 UTILS
['make'] = os
.getenv('MAKE')
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.'''
205 def execute(args
: list[str], verbose
: int) -> None:
206 '''Execute the given shell command.'''
208 print('executing %s' % ' '.join(args
), flush
=True)
210 retcode
= sp
.call(args
)
211 except Exception as error
:
212 sys
.stderr
.write(str(error
) + '\n')
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
)
220 retcode
= sp
.call(xargs
, shell
=True)
221 except Exception as error
:
222 sys
.stderr
.write(str(error
) + '\n')
227 print('executing %s' % ' '.join(args
))
228 with
open(temp
, mode
='r', newline
='\n', encoding
='utf-8') as file:
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
]
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
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.'''
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
)]
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
)]
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
)
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.
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.'''
310 # This should work too.
311 return relativize(dir, '.')
314 for component
in dir.split('/'):
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
)
325 shutil
.copymode(src
, dest
)
326 except PermissionError
:
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
)
336 shutil
.copystat(src
, dest
)
337 except PermissionError
:
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.'''
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
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.'''
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
):
365 # src is relative to the directory of dest.
366 last_slash
= dest
.rfind('/')
368 cp_src
= joinpath(dest
[0:last_slash
-1], 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
):
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
)
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
):
412 except FileNotFoundError
:
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.'''
423 except PermissionError
:
424 sys
.stderr
.write('%s: ln failed; falling back on cp -p\n' % APP
['name'])
425 if os
.path
.isabs(src
):
429 # src is relative to the directory of dest.
430 last_slash
= dest
.rfind('/')
432 cp_src
= joinpath(dest
[0: last_slash
- 1], 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.
445 sp
.run(['rm', '-rf', dest
], shell
=False)
449 except FileNotFoundError
:
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.'''
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.
471 result
= separator
.join(listing
)
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.'''
481 return '\n'.join(lines
) + '\n'
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.'''
492 if data
.startswith(orig
):
493 result
= repl
+ data
[len(orig
):]
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.'''
503 if data
.endswith(orig
):
504 result
= data
[:-len(orig
)] + repl
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.'''
512 while result
.endswith('/'):
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,
535 match
= pattern
.search(text
, outerpos
)
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)
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
:]
549 match
= pattern
.search(text
, outerpos
)
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.'''
558 value
= sp
.run(['tput', capability
], stdout
=sp
.PIPE
, stderr
=sp
.DEVNULL
).stdout
.decode('utf-8')
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.
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')
580 bold_on
= get_terminfo_string('bold')
581 bold_off
= get_terminfo_string('sgr0')
582 if bold_on
== '' or bold_off
== '':
588 return (bold_on
, bold_off
)
591 __all__
+= ['APP', 'DIRS', 'MODES', 'UTILS']