I never really looked at the joinpath() function so I just realized it
essentially does os.path.normpath(os.path.join(...)) unlike what it's
doc string says.

I have applied this patch removing it and preferring os.path.join()
which should help make things more clear IMO. The os.path.normpath()
isn't really necessary. I've added it in one place to make a stdout
message go from:

   ./tests/test-stdbool.c -> tests/test-stdbool.c

It passes all the test cases, but for some projects maybe the output
might be different. For example, A/./B not being simplified to A/B
which shouldn't cause any issues building.

Collin
From 790206bfe741d1280b0b5734a0d4070b26a885ab Mon Sep 17 00:00:00 2001
From: Collin Funk <collin.fu...@gmail.com>
Date: Fri, 14 Jun 2024 21:55:48 -0700
Subject: [PATCH] gnulib-tool.py: Simplify joining paths.

* pygnulib/constants.py (joinpath): Remove function. It is equivalent to
os.path.normpath(os.path.join(...)) where the os.path.normpath is
typically not needed.
(relativize, symlink_relative, as_link_value_at_dest, hardlink): Use
os.path.join instead of joinpath.
* pygnulib/GLConfig.py (resetAutoconfFile): Likewise.
* pygnulib/GLEmiter.py (lib_Makefile_am, tests_Makefile_am): Likewise.
* pygnulib/GLFileSystem.py (lookup, shouldLink, tmpfilename, add)
(update, add_or_update, super_update): Likewise.
* pygnulib/GLInfo.py (version): Likewise.
* pygnulib/GLMakefileTable.py (parent): Likewise.
* pygnulib/GLTestDir.py (_patch_test_driver, execute): Likewise.
* pygnulib/main.py (main): Likewise.
* pygnulib/GLModuleSystem.py (exists, find, getFiles): Likewise.
* pygnulib/GLImport.py (__init__, relative_to_currdir)
(_done_dir_, _update_ignorelist_, prepare, execute): Likewise.
(getAutomakeSnippet_Unconditional): Likewise. Add a comment about not
using os.path.normpath() to protect a Make variable.
---
 ChangeLog                   | 22 ++++++++++
 pygnulib/GLConfig.py        |  9 ++---
 pygnulib/GLEmiter.py        |  9 ++---
 pygnulib/GLFileSystem.py    | 33 ++++++++-------
 pygnulib/GLImport.py        | 81 ++++++++++++++++++-------------------
 pygnulib/GLInfo.py          |  4 +-
 pygnulib/GLMakefileTable.py | 11 +++--
 pygnulib/GLModuleSystem.py  | 18 ++++-----
 pygnulib/GLTestDir.py       | 75 +++++++++++++++++-----------------
 pygnulib/constants.py       | 26 +++---------
 pygnulib/main.py            | 25 ++++++------
 11 files changed, 157 insertions(+), 156 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 3f5d7454fb..12b392648d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,25 @@
+2024-06-14  Collin Funk  <collin.fu...@gmail.com>
+
+	gnulib-tool.py: Simplify joining paths.
+	* pygnulib/constants.py (joinpath): Remove function. It is equivalent to
+	os.path.normpath(os.path.join(...)) where the os.path.normpath is
+	typically not needed.
+	(relativize, symlink_relative, as_link_value_at_dest, hardlink): Use
+	os.path.join instead of joinpath.
+	* pygnulib/GLConfig.py (resetAutoconfFile): Likewise.
+	* pygnulib/GLEmiter.py (lib_Makefile_am, tests_Makefile_am): Likewise.
+	* pygnulib/GLFileSystem.py (lookup, shouldLink, tmpfilename, add)
+	(update, add_or_update, super_update): Likewise.
+	* pygnulib/GLInfo.py (version): Likewise.
+	* pygnulib/GLMakefileTable.py (parent): Likewise.
+	* pygnulib/GLTestDir.py (_patch_test_driver, execute): Likewise.
+	* pygnulib/main.py (main): Likewise.
+	* pygnulib/GLModuleSystem.py (exists, find, getFiles): Likewise.
+	* pygnulib/GLImport.py (__init__, relative_to_currdir)
+	(_done_dir_, _update_ignorelist_, prepare, execute): Likewise.
+	(getAutomakeSnippet_Unconditional): Likewise. Add a comment about not
+	using os.path.normpath() to protect a Make variable.
+
 2024-06-14  Bruno Haible  <br...@clisp.org>
 
 	doc: Update for glibc 2.35.
diff --git a/pygnulib/GLConfig.py b/pygnulib/GLConfig.py
index b8a7fc5b0b..110bdf3b1d 100644
--- a/pygnulib/GLConfig.py
+++ b/pygnulib/GLConfig.py
@@ -24,7 +24,6 @@
 from .constants import (
     MODES,
     TESTS,
-    joinpath,
     remove_trailing_slashes,
 )
 from .GLError import GLError
@@ -1048,10 +1047,10 @@ def setAutoconfFile(self, configure_ac: str) -> None:
     def resetAutoconfFile(self) -> None:
         '''Reset path of autoconf file relative to destdir.'''
         configure_ac = ''
-        if os.path.isfile(joinpath(self.table['destdir'], 'configure.ac')):
-            configure_ac = joinpath(self.table['destdir'], 'configure.ac')
-        elif os.path.isfile(joinpath(self.table['destdir'], 'configure.in')):
-            configure_ac = joinpath(self.table['destdir'], 'configure.in')
+        if os.path.isfile(os.path.join(self.table['destdir'], 'configure.ac')):
+            configure_ac = os.path.join(self.table['destdir'], 'configure.ac')
+        elif os.path.isfile(os.path.join(self.table['destdir'], 'configure.in')):
+            configure_ac = os.path.join(self.table['destdir'], 'configure.in')
         self.table['configure_ac'] = configure_ac
 
     # Define ac_version methods.
diff --git a/pygnulib/GLEmiter.py b/pygnulib/GLEmiter.py
index 38b23bbfcf..e03074eb61 100644
--- a/pygnulib/GLEmiter.py
+++ b/pygnulib/GLEmiter.py
@@ -24,7 +24,6 @@
 from collections.abc import Callable
 from .constants import (
     UTILS,
-    joinpath,
     lines_to_multiline,
     combine_lines_matching,
     substart,
@@ -825,7 +824,7 @@ def lib_Makefile_am(self, destfile: str, modules: list[GLModule], moduletable: G
         if gnu_make:
             emit += '# Start of GNU Make output.\n'
             result = sp.run([UTILS['autoconf'], '-t', 'AC_SUBST:$1 = @$1@',
-                            joinpath(self.config['destdir'], 'configure.ac')],
+                             os.path.join(self.config['destdir'], 'configure.ac')],
                             capture_output=True)
             if result.returncode == 0:
                 # sort -u
@@ -843,7 +842,7 @@ def lib_Makefile_am(self, destfile: str, modules: list[GLModule], moduletable: G
         for current_edit in range(0, makefiletable.count()):
             dictionary = makefiletable[current_edit]
             if 'var' in dictionary:
-                if destfile == joinpath(dictionary['dir'], 'Makefile.am'):
+                if destfile == os.path.join(dictionary['dir'], 'Makefile.am'):
                     val = dictionary['val']
                     if dictionary['var'] == 'SUBDIRS' and dictionary['dotfirst']:
                         # The added subdirectory ${val} needs to be mentioned after '.'.
@@ -882,7 +881,7 @@ def lib_Makefile_am(self, destfile: str, modules: list[GLModule], moduletable: G
         else:
             # Then test if $sourcebase/Makefile.am (if it exists) specifies it.
             if makefile_name:
-                path = joinpath(sourcebase, 'Makefile.am')
+                path = os.path.join(sourcebase, 'Makefile.am')
                 if os.path.isfile(path):
                     with open(path, mode='r', newline='\n', encoding='utf-8') as file:
                         data = file.read()
@@ -1140,7 +1139,7 @@ def tests_Makefile_am(self, destfile: str, modules: list[GLModule], moduletable:
         for current_edit in range(0, makefiletable.count()):
             dictionary = makefiletable[current_edit]
             if 'var' in dictionary:
-                if destfile == joinpath(dictionary['dir'], 'Makefile.am'):
+                if destfile == os.path.join(dictionary['dir'], 'Makefile.am'):
                     val = dictionary['val']
                     if dictionary['var'] == 'SUBDIRS' and dictionary['dotfirst']:
                         # The added subdirectory ${val} needs to be mentioned after '.'.
diff --git a/pygnulib/GLFileSystem.py b/pygnulib/GLFileSystem.py
index f2a98a0a28..4666fcdffb 100644
--- a/pygnulib/GLFileSystem.py
+++ b/pygnulib/GLFileSystem.py
@@ -28,7 +28,6 @@
     DIRS,
     ensure_writable,
     hardlink,
-    joinpath,
     link_if_changed,
     movefile,
     copyfile,
@@ -80,23 +79,23 @@ def lookup(self, name: str) -> tuple[str, bool]:
         lookedupFile = None
         lookedupPatches = []
         for localdir in localpath:
-            file_in_localdir = joinpath(localdir, name)
+            file_in_localdir = os.path.join(localdir, name)
             if os.path.isfile(file_in_localdir):
                 lookedupFile = file_in_localdir
                 break
-            diff_in_localdir = joinpath(localdir, '%s.diff' % name)
+            diff_in_localdir = os.path.join(localdir, '%s.diff' % name)
             if os.path.isfile(diff_in_localdir):
                 lookedupPatches.append(diff_in_localdir)
         # Treat the gnulib dir like a lowest-priority --local-dir, except that
         # here we don't look for .diff files.
         if lookedupFile == None:
-            file_in_localdir = joinpath(DIRS['root'], name)
+            file_in_localdir = os.path.join(DIRS['root'], name)
             if os.path.isfile(file_in_localdir):
                 lookedupFile = file_in_localdir
         if lookedupFile != None:
             if len(lookedupPatches) > 0:
                 # Apply the patches, from lowest-priority to highest-priority.
-                tempFile = joinpath(self.config['tempdir'], name)
+                tempFile = os.path.join(self.config['tempdir'], name)
                 try:  # Try to create directories
                     os.makedirs(os.path.dirname(tempFile))
                 except OSError:
@@ -132,7 +131,7 @@ def shouldLink(self, original: str, lookedup: str) -> bool:
         # action anyways.
         if copymode != lcopymode:
             for localdir in localpath:
-                if lookedup == joinpath(localdir, original):
+                if lookedup == os.path.join(localdir, original):
                     return lcopymode
         return copymode
 
@@ -193,7 +192,7 @@ def tmpfilename(self, path: str) -> str:
         if not self.config['dryrun']:
             # Put the new contents of $file in a file in the same directory (needed
             # to guarantee that an 'mv' to "$destdir/$file" works).
-            result = joinpath(self.config['destdir'], '%s.tmp' % path)
+            result = os.path.join(self.config['destdir'], '%s.tmp' % path)
             dirname = os.path.dirname(result)
             if dirname and not os.path.isdir(dirname):
                 os.makedirs(dirname)
@@ -201,7 +200,7 @@ def tmpfilename(self, path: str) -> str:
             # Put the new contents of $file in a file in a temporary directory
             # (because the directory of "$file" might not exist).
             tempdir = self.config['tempdir']
-            result = joinpath(tempdir, '%s.tmp' % os.path.basename(path))
+            result = os.path.join(tempdir, '%s.tmp' % os.path.basename(path))
             dirname = os.path.dirname(result)
             if not os.path.isdir(dirname):
                 os.makedirs(dirname)
@@ -245,14 +244,14 @@ def add(self, lookedup: str, tmpflag: bool, tmpfile: str) -> None:
             print('Copying file %s' % rewritten)
             if self.filesystem.shouldLink(original, lookedup) == CopyAction.Symlink \
                     and not tmpflag and filecmp.cmp(lookedup, tmpfile):
-                link_if_changed(lookedup, joinpath(destdir, rewritten))
+                link_if_changed(lookedup, os.path.join(destdir, rewritten))
             else:  # if any of these conditions is not met
                 if self.filesystem.shouldLink(original, lookedup) == CopyAction.Hardlink \
                    and not tmpflag and filecmp.cmp(lookedup, tmpfile):
-                    hardlink(lookedup, joinpath(destdir, rewritten))
+                    hardlink(lookedup, os.path.join(destdir, rewritten))
                 else:  # Move instead of linking.
                     try:  # Try to move file
-                        movefile(tmpfile, joinpath(destdir, rewritten))
+                        movefile(tmpfile, os.path.join(destdir, rewritten))
                     except Exception as exc:
                         raise GLError(17, original) from exc
         else:  # if self.config['dryrun']
@@ -277,8 +276,8 @@ def update(self, lookedup: str, tmpflag: bool, tmpfile: str, already_present: bo
                             % type(already_present).__name__)
         basename = rewritten
         backupname = '%s~' % basename
-        basepath = joinpath(destdir, basename)
-        backuppath = joinpath(destdir, backupname)
+        basepath = os.path.join(destdir, basename)
+        backuppath = os.path.join(destdir, backupname)
         if not filecmp.cmp(basepath, tmpfile):
             if not self.config['dryrun']:
                 if already_present:
@@ -305,7 +304,7 @@ def update(self, lookedup: str, tmpflag: bool, tmpfile: str, already_present: bo
                         try:  # Try to move file
                             if os.path.exists(basepath):
                                 os.remove(basepath)
-                            copyfile(tmpfile, joinpath(destdir, rewritten))
+                            copyfile(tmpfile, os.path.join(destdir, rewritten))
                         except Exception as exc:
                             raise GLError(17, original) from exc
             else:  # if self.config['dryrun']
@@ -357,7 +356,7 @@ def add_or_update(self, already_present: bool) -> None:
                 # Write the transformed data to the temporary file.
                 with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
                     file.write(re.sub(transformer[0], transformer[1], src_data))
-        path = joinpath(self.config['destdir'], rewritten)
+        path = os.path.join(self.config['destdir'], rewritten)
         if os.path.isfile(path):
             # The file already exists.
             self.update(lookedup, tmpflag, tmpfile, already_present)
@@ -377,8 +376,8 @@ def super_update(self, basename: str, tmpfile: str) -> tuple[str, str, int]:
           1: tmpfile was used to update destfile;
           2: destfile was created, because it didn't exist.'''
         backupname = '%s~' % basename
-        basepath = joinpath(self.config['destdir'], basename)
-        backuppath = joinpath(self.config['destdir'], backupname)
+        basepath = os.path.join(self.config['destdir'], basename)
+        backuppath = os.path.join(self.config['destdir'], backupname)
         if os.path.isfile(basepath):
             if filecmp.cmp(basepath, tmpfile):
                 result_flag = 0
diff --git a/pygnulib/GLImport.py b/pygnulib/GLImport.py
index 5f090d4ea1..db8836d4c9 100644
--- a/pygnulib/GLImport.py
+++ b/pygnulib/GLImport.py
@@ -27,7 +27,6 @@
     MODES,
     TESTS,
     cleaner,
-    joinpath,
     lines_to_multiline,
     movefile,
     copyfile,
@@ -128,7 +127,7 @@ def __init__(self, config: GLConfig, mode: int, m4dirs: list[str]) -> None:
                 raise GLError(4, version)
 
         # Get other cached variables.
-        path = joinpath(self.config['m4base'], 'gnulib-cache.m4')
+        path = os.path.join(self.config['m4base'], 'gnulib-cache.m4')
         if os.path.isfile(path):
             with open(path, mode='r', newline='\n', encoding='utf-8') as file:
                 data = file.read()
@@ -222,7 +221,7 @@ def __init__(self, config: GLConfig, mode: int, m4dirs: list[str]) -> None:
 
             # Get cached filelist from gnulib-comp.m4.
             destdir, m4base = self.config.getDestDir(), self.config.getM4Base()
-            path = joinpath(destdir, m4base, 'gnulib-comp.m4')
+            path = os.path.join(destdir, m4base, 'gnulib-comp.m4')
             if os.path.isfile(path):
                 with open(path, mode='r', newline='\n', encoding='utf-8') as file:
                     data = file.read()
@@ -291,8 +290,8 @@ def __init__(self, config: GLConfig, mode: int, m4dirs: list[str]) -> None:
                     base = self.config['destdir']
                 else:
                     base = '.'
-                if os.path.isfile(joinpath(base, 'Makefile.am')):
-                    with open(joinpath(base, 'Makefile.am'), mode='r', newline='\n', encoding='utf-8') as file:
+                if os.path.isfile(os.path.join(base, 'Makefile.am')):
+                    with open(os.path.join(base, 'Makefile.am'), mode='r', newline='\n', encoding='utf-8') as file:
                         data = file.read()
                     pattern = re.compile(r'^AUTOMAKE_OPTIONS[\t ]*=(.*)$', re.MULTILINE)
                     automake_options = pattern.findall(data)
@@ -438,7 +437,7 @@ def relative_to_currdir(self, dir: str) -> str:
         else:
             if os.path.isabs(destdir):
                 # XXX This doesn't look right.
-                return joinpath(destdir, dir)
+                return os.path.join(destdir, dir)
             else:
                 return relconcat(destdir, dir)
 
@@ -676,14 +675,14 @@ def _done_dir_(self, directory: str, files_added: list[str], files_removed: list
         '''This method is used to determine ignore argument for _update_ignorelist_
         method and then call it.'''
         destdir = self.config['destdir']
-        if (os.path.isdir(joinpath(destdir, 'CVS'))
-                or os.path.isdir(joinpath(destdir, directory, 'CVS'))
-                or os.path.isfile(joinpath(destdir, directory, '.cvsignore'))):
+        if (os.path.isdir(os.path.join(destdir, 'CVS'))
+                or os.path.isdir(os.path.join(destdir, directory, 'CVS'))
+                or os.path.isfile(os.path.join(destdir, directory, '.cvsignore'))):
             self._update_ignorelist_(directory, '.cvsignore',
                                      files_added, files_removed)
-        if (os.path.isdir(joinpath(destdir, '.git'))
-                or os.path.isfile(joinpath(destdir, '.gitignore'))
-                or os.path.isfile(joinpath(destdir, directory, '.gitignore'))):
+        if (os.path.isdir(os.path.join(destdir, '.git'))
+                or os.path.isfile(os.path.join(destdir, '.gitignore'))
+                or os.path.isfile(os.path.join(destdir, directory, '.gitignore'))):
             self._update_ignorelist_(directory, '.gitignore',
                                      files_added, files_removed)
 
@@ -697,11 +696,11 @@ def _update_ignorelist_(self, directory: str, ignore: str, files_added: list[str
             anchor = '/'
         else:
             anchor = ''
-        srcpath = joinpath(directory, ignore)
+        srcpath = os.path.join(directory, ignore)
         backupname = '%s~' % srcpath
-        if os.path.isfile(joinpath(destdir, srcpath)):
+        if os.path.isfile(os.path.join(destdir, srcpath)):
             if files_added or files_removed:
-                with open(joinpath(destdir, srcpath), mode='r', newline='\n', encoding='utf-8') as file:
+                with open(os.path.join(destdir, srcpath), mode='r', newline='\n', encoding='utf-8') as file:
                     original_lines = file.readlines()
                 # Clean the newlines but not trailing whitespace.
                 original_lines = [ line.rstrip('\n')
@@ -714,7 +713,7 @@ def _update_ignorelist_(self, directory: str, ignore: str, files_added: list[str
                 if filenames_to_add or filenames_to_remove:
                     if not self.config['dryrun']:
                         print('Updating %s (backup in %s)' % (srcpath, backupname))
-                        copyfile2(joinpath(destdir, srcpath), joinpath(destdir, backupname))
+                        copyfile2(os.path.join(destdir, srcpath), os.path.join(destdir, backupname))
                         new_lines = original_lines + [ f'{anchor}{filename}'
                                                        for filename in sorted(filenames_to_add) ]
                         if anchor != '':
@@ -725,11 +724,11 @@ def _update_ignorelist_(self, directory: str, ignore: str, files_added: list[str
                         new_lines = [ line
                                       for line in new_lines
                                       if line not in lines_to_remove ]
-                        with open(joinpath(destdir, srcpath), mode='w', newline='\n', encoding='utf-8') as file:
+                        with open(os.path.join(destdir, srcpath), mode='w', newline='\n', encoding='utf-8') as file:
                             file.write(lines_to_multiline(new_lines))
                     else:  # if self.config['dryrun']
                         print('Update %s (backup in %s)' % (srcpath, backupname))
-        else:  # if not os.path.isfile(joinpath(destdir, srcpath))
+        else:  # if not os.path.isfile(os.path.join(destdir, srcpath))
             if files_added:
                 if not self.config['dryrun']:
                     print('Creating %s' % srcpath)
@@ -739,7 +738,7 @@ def _update_ignorelist_(self, directory: str, ignore: str, files_added: list[str
                     if ignore == '.cvsignore':
                         # Automake generates Makefile rules that create .dirstamp files.
                         files_added = ['.deps', '.dirstamp'] + files_added
-                    with open(joinpath(destdir, srcpath), mode='w', newline='\n', encoding='utf-8') as file:
+                    with open(os.path.join(destdir, srcpath), mode='w', newline='\n', encoding='utf-8') as file:
                         file.write(lines_to_multiline(files_added))
                 else:  # if self.config['dryrun']
                     print('Create %s' % srcpath)
@@ -877,9 +876,9 @@ def prepare(self) -> tuple[GLFileTable, dict[str, tuple[re.Pattern, str] | None]
         # Add m4/gnulib-tool.m4 to the file list. It is not part of any module.
         new_files = filelist + ['m4/gnulib-tool.m4']
         old_files = list(self.cache['files'])
-        path = joinpath(destdir, m4base, 'gnulib-tool.m4')
+        path = os.path.join(destdir, m4base, 'gnulib-tool.m4')
         if os.path.isfile(path):
-            old_files.append(joinpath('m4', 'gnulib-tool.m4'))
+            old_files.append(os.path.join('m4', 'gnulib-tool.m4'))
         # old_files is the list of files according to the last gnulib-tool invocation.
         # new_files is the list of files after this gnulib-tool invocation.
 
@@ -982,7 +981,7 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
         pairs = sorted(set(pairs), key=lambda pair: pair[0])
         files = sorted(set(pair[0] for pair in pairs))
         for file in files:
-            path = joinpath(destdir, file)
+            path = os.path.normpath(os.path.join(destdir, file))
             if os.path.isfile(path) or os.path.islink(path):
                 if not self.config['dryrun']:
                     backup = '%s~' % path
@@ -1067,11 +1066,11 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
         if pobase:
             # Create po makefile and auxiliary files.
             for file in ['Makefile.in.in', 'remove-potcdate.sin']:
-                tmpfile = assistant.tmpfilename(joinpath(pobase, file))
-                path = joinpath('build-aux', 'po', file)
+                tmpfile = assistant.tmpfilename(os.path.join(pobase, file))
+                path = os.path.join('build-aux', 'po', file)
                 lookedup, flag = filesystem.lookup(path)
                 copyfile(lookedup, tmpfile)
-                basename = joinpath(pobase, file)
+                basename = os.path.join(pobase, file)
                 filename, backup, flag = assistant.super_update(basename, tmpfile)
                 if flag == 1:
                     if not self.config['dryrun']:
@@ -1088,7 +1087,7 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
                     os.remove(tmpfile)
 
             # Create po makefile parameterization, part 1.
-            basename = joinpath(pobase, 'Makevars')
+            basename = os.path.join(pobase, 'Makevars')
             tmpfile = assistant.tmpfilename(basename)
             emit = self.emitter.po_Makevars()
             with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
@@ -1109,11 +1108,11 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
                 os.remove(tmpfile)
 
             # Create po makefile parameterization, part 2.
-            basename = joinpath(pobase, 'POTFILES.in')
+            basename = os.path.join(pobase, 'POTFILES.in')
             tmpfile = assistant.tmpfilename(basename)
             with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
                 file.write(self.emitter.po_POTFILES_in(filetable.all_files))
-            basename = joinpath(pobase, 'POTFILES.in')
+            basename = os.path.join(pobase, 'POTFILES.in')
             filename, backup, flag = assistant.super_update(basename, tmpfile)
             if flag == 1:
                 if not self.config['dryrun']:
@@ -1135,17 +1134,17 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
                 print('Fetching gnulib PO files from %s' % TP_URL)
                 args = ['wget', '--no-verbose', '--mirror', '--level=1', '-nd', '-A.po', '-P', '.',
                         '%sgnulib/' % TP_URL]
-                sp.call(args, cwd=joinpath(destdir, pobase))
+                sp.call(args, cwd=os.path.join(destdir, pobase))
             else:  # if self.config['dryrun']
                 print('Fetch gnulib PO files from %s' % TP_URL)
 
             # Create po/LINGUAS.
-            basename = joinpath(pobase, 'LINGUAS')
+            basename = os.path.join(pobase, 'LINGUAS')
             if not self.config['dryrun']:
                 tmpfile = assistant.tmpfilename(basename)
                 data = '# Set of available languages.\n'
                 files = sorted([ subend('.po', '', file)
-                                 for file in os.listdir(joinpath(destdir, pobase))
+                                 for file in os.listdir(os.path.join(destdir, pobase))
                                  if file.endswith('.po') ])
                 data += lines_to_multiline(files)
                 with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
@@ -1160,13 +1159,13 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
                     os.remove(tmpfile)
             else:  # if not self.config['dryrun']
                 backupname = '%s~' % basename
-                if os.path.isfile(joinpath(destdir, basename)):
+                if os.path.isfile(os.path.join(destdir, basename)):
                     print('Update %s (backup in %s)' % (basename, backupname))
-                else:  # if not os.path.isfile(joinpath(destdir, basename))
+                else:  # if not os.path.isfile(os.path.join(destdir, basename))
                     print('Create %s' % basename)
 
         # Create m4/gnulib-cache.m4.
-        basename = joinpath(m4base, 'gnulib-cache.m4')
+        basename = os.path.join(m4base, 'gnulib-cache.m4')
         tmpfile = assistant.tmpfilename(basename)
         emit = self.gnulib_cache()
         with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
@@ -1191,7 +1190,7 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
             os.remove(tmpfile)
 
         # Create m4/gnulib-comp.m4.
-        basename = joinpath(m4base, 'gnulib-comp.m4')
+        basename = os.path.join(m4base, 'gnulib-comp.m4')
         tmpfile = assistant.tmpfilename(basename)
         emit = self.gnulib_comp(filetable, gentests)
         with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
@@ -1229,13 +1228,13 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
         # Create library makefile.
         # Do this after creating gnulib-comp.m4, because func_emit_lib_Makefile_am
         # can run 'autoconf -t', which reads gnulib-comp.m4.
-        basename = joinpath(sourcebase, source_makefile_am)
+        basename = os.path.join(sourcebase, source_makefile_am)
         tmpfile = assistant.tmpfilename(basename)
         emit = self.emitter.lib_Makefile_am(basename, self.moduletable.getMainModules(),
                                             self.moduletable, self.makefiletable,
                                             actioncmd, for_test)
         if automake_subdir:
-            emit = sp.run([joinpath(DIRS['root'], 'build-aux/prefix-gnulib-mk'), '--from-gnulib-tool',
+            emit = sp.run([os.path.join(DIRS['root'], 'build-aux/prefix-gnulib-mk'), '--from-gnulib-tool',
                            f'--lib-name={libname}', f'--prefix={sourcebase}/'],
                           input=emit, text=True, capture_output=True).stdout
         with open(tmpfile, mode='w', newline='\n', encoding='utf-8') as file:
@@ -1257,7 +1256,7 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
 
         # Create tests Makefile.
         if gentests:
-            basename = joinpath(testsbase, tests_makefile_am)
+            basename = os.path.join(testsbase, tests_makefile_am)
             tmpfile = assistant.tmpfilename(basename)
             emit = self.emitter.tests_Makefile_am(basename, self.moduletable.getTestsModules(),
                                                   self.moduletable, self.makefiletable,
@@ -1283,7 +1282,7 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
             # Update the .cvsignore and .gitignore files.
             ignorelist = []
             # Treat gnulib-comp.m4 like an added file, even if it already existed.
-            filetable.added_files.append(joinpath(m4base, 'gnulib-comp.m4'))
+            filetable.added_files.append(os.path.join(m4base, 'gnulib-comp.m4'))
             filetable.added_files = sorted(set(filetable.added_files))
             filetable.removed_files = sorted(set(filetable.removed_files))
             for file in filetable.added_files:
@@ -1382,12 +1381,12 @@ def execute(self, filetable: GLFileTable, transformers: dict[str, tuple[re.Patte
             if 'var' in dictionary:
                 if dictionary['var'] == 'ACLOCAL_AMFLAGS':
                     print('  - mention "-I %s" in %s in %s'
-                          % (dictionary['val'], dictionary['var'], joinpath(dictionary['dir'], 'Makefile.am')))
+                          % (dictionary['val'], dictionary['var'], os.path.join(dictionary['dir'], 'Makefile.am')))
                     print('    or add an AC_CONFIG_MACRO_DIRS([%s]) invocation in %s,'
                           % (dictionary['val'], configure_ac))
                 else:
                     print('  - mention "%s" in %s in %s,'
-                          % (dictionary['val'], dictionary['var'], joinpath(dictionary['dir'], 'Makefile.am')))
+                          % (dictionary['val'], dictionary['var'], os.path.join(dictionary['dir'], 'Makefile.am')))
 
         # Detect position_early_after.
         with open(configure_ac, mode='r', newline='\n', encoding='utf-8') as file:
diff --git a/pygnulib/GLInfo.py b/pygnulib/GLInfo.py
index eac4c65071..19bea7ecb3 100644
--- a/pygnulib/GLInfo.py
+++ b/pygnulib/GLInfo.py
@@ -22,7 +22,7 @@
 import re
 import subprocess as sp
 from pygnulib import __author__, __copyright__
-from .constants import DIRS, joinpath
+from .constants import DIRS
 
 
 #===============================================================================
@@ -321,7 +321,7 @@ def version(self) -> str:
             except:
                 have_git = False
             if have_git:
-                version_gen = joinpath(DIRS['build-aux'], 'git-version-gen')
+                version_gen = os.path.join(DIRS['build-aux'], 'git-version-gen')
                 args = [version_gen, '/dev/null']
                 result = sp.check_output(args, cwd=DIRS['root']).decode('UTF-8')
                 result = result.strip()
diff --git a/pygnulib/GLMakefileTable.py b/pygnulib/GLMakefileTable.py
index efd276d20c..d0151d9b04 100644
--- a/pygnulib/GLMakefileTable.py
+++ b/pygnulib/GLMakefileTable.py
@@ -19,7 +19,6 @@
 # Define global imports
 #===============================================================================
 import os
-from .constants import joinpath
 from .GLConfig import GLConfig
 
 #===============================================================================
@@ -91,12 +90,12 @@ def parent(self, gentests: bool, source_makefile_am: str, tests_makefile_am: str
         dir1 = '%s%s' % (m4base, os.path.sep)
         dir2 = ''
         while (dir1
-               and not (os.path.isfile(joinpath(self.config['destdir'], dir1, 'Makefile.am'))
-                        or joinpath(dir1, 'Makefile.am') == joinpath(sourcebase, source_makefile_am)
-                        or (gentests and joinpath(dir1, 'Makefile.am') == joinpath(testsbase, tests_makefile_am)))):
-            dir2 = joinpath(os.path.basename(dir1), dir2)
+               and not (os.path.isfile(os.path.join(self.config['destdir'], dir1, 'Makefile.am'))
+                        or os.path.join(dir1, 'Makefile.am') == os.path.join(sourcebase, source_makefile_am)
+                        or (gentests and os.path.join(dir1, 'Makefile.am') == os.path.join(testsbase, tests_makefile_am)))):
+            dir2 = os.path.join(os.path.basename(dir1), dir2)
             dir1 = os.path.dirname(dir1)
-        self.editor(dir1, 'EXTRA_DIST', joinpath(dir2, 'gnulib-cache.m4'))
+        self.editor(dir1, 'EXTRA_DIST', os.path.join(dir2, 'gnulib-cache.m4'))
 
     def count(self) -> int:
         '''Count number of edits which are stored, including the removed ones.'''
diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py
index 02dacfcf9f..4b1eed8394 100644
--- a/pygnulib/GLModuleSystem.py
+++ b/pygnulib/GLModuleSystem.py
@@ -31,7 +31,6 @@
     TESTS,
     combine_lines,
     filter_filelist,
-    joinpath,
     lines_to_multiline,
     subend,
 )
@@ -91,11 +90,11 @@ def exists(self, module_name: str) -> bool:
         badnames = ['ChangeLog', 'COPYING', 'README', 'TEMPLATE',
                     'TEMPLATE-EXTENDED', 'TEMPLATE-TESTS']
         if module_name not in badnames:
-            result = os.path.isfile(joinpath(DIRS['modules'], module_name))
+            result = os.path.isfile(os.path.join(DIRS['modules'], module_name))
             if not result:
                 for localdir in localpath:
-                    if (os.path.isdir(joinpath(localdir, 'modules'))
-                            and os.path.isfile(joinpath(localdir, 'modules', module_name))):
+                    if (os.path.isdir(os.path.join(localdir, 'modules'))
+                            and os.path.isfile(os.path.join(localdir, 'modules', module_name))):
                         result = True
                         break
         return result
@@ -108,7 +107,7 @@ def find(self, module_name: str) -> GLModule | None:
             raise TypeError('module_name must be a string, not %s'
                             % type(module_name).__name__)
         if self.exists(module_name):
-            path, istemp = self.filesystem.lookup(joinpath('modules', module_name))
+            path, istemp = self.filesystem.lookup(os.path.join('modules', module_name))
             result = GLModule(self.config, module_name, path, istemp)
             return result
         else:  # if not self.exists(module)
@@ -470,9 +469,9 @@ def getFiles(self) -> list[str]:
             result = [ line.strip()
                        for line in snippet.split('\n')
                        if line.strip() ]
-            result.append(joinpath('m4', '00gnulib.m4'))
-            result.append(joinpath('m4', 'zzgnulib.m4'))
-            result.append(joinpath('m4', 'gnulib-common.m4'))
+            result.append(os.path.join('m4', '00gnulib.m4'))
+            result.append(os.path.join('m4', 'zzgnulib.m4'))
+            result.append(os.path.join('m4', 'gnulib-common.m4'))
             self.cache['files'] = result
         return self.cache['files']
 
@@ -621,7 +620,8 @@ def getAutomakeSnippet_Unconditional(self) -> str:
                 buildaux_files = filter_filelist('\n', all_files,
                                                  'build-aux/', '', 'build-aux/', '')
                 if buildaux_files != '':
-                    buildaux_files = [ os.path.join('$(top_srcdir)', joinpath(auxdir, filename))
+                    # Don't let os.path.normpath() remove $(top_srcdir) when auxdir starts with '..'.
+                    buildaux_files = [ os.path.join('$(top_srcdir)', os.path.normpath(os.path.join(auxdir, filename)))
                                        for filename in buildaux_files.split('\n') ]
                     result += 'EXTRA_DIST += %s' % ' '.join(buildaux_files)
                     result += '\n\n'
diff --git a/pygnulib/GLTestDir.py b/pygnulib/GLTestDir.py
index ad4549f734..72150afbc5 100644
--- a/pygnulib/GLTestDir.py
+++ b/pygnulib/GLTestDir.py
@@ -33,7 +33,6 @@
     ensure_writable,
     force_output,
     hardlink,
-    joinpath,
     link_relative,
     lines_to_multiline,
     movefile,
@@ -56,11 +55,11 @@
 
 def _patch_test_driver() -> None:
     '''Patch the test-driver script in testdirs.'''
-    test_driver = joinpath('build-aux', 'test-driver')
+    test_driver = os.path.join('build-aux', 'test-driver')
     print('patching file %s' % test_driver)
-    diffs = [ joinpath(DIRS['root'], name)
-              for name in [joinpath('build-aux', 'test-driver.diff'),
-                           joinpath('build-aux', 'test-driver-1.16.3.diff')] ]
+    diffs = [ os.path.join(DIRS['root'], name)
+              for name in [os.path.join('build-aux', 'test-driver.diff'),
+                           os.path.join('build-aux', 'test-driver-1.16.3.diff')] ]
     patched = False
     for diff in diffs:
         command = f'patch {shlex.quote(test_driver)} < {shlex.quote(diff)}'
@@ -321,7 +320,7 @@ def execute(self) -> None:
         filetable.new_files = sorted(new_table, key=lambda pair: pair[0])
 
         # Create directories.
-        directories = sorted({ joinpath(self.testdir, os.path.dirname(pair[0]))
+        directories = sorted({ os.path.join(self.testdir, os.path.dirname(pair[0]))
                                for pair in filetable.new_files })
         for directory in directories:
             if not os.path.isdir(directory):
@@ -329,7 +328,7 @@ def execute(self) -> None:
 
         # Copy files or make symbolic links or hard links.
         for (dest, src) in filetable.new_files:
-            destpath = joinpath(self.testdir, dest)
+            destpath = os.path.join(self.testdir, dest)
             if src.startswith('tests=lib/'):
                 src = substart('tests=lib/', 'lib/', src)
             lookedup, flag = self.filesystem.lookup(src)
@@ -349,10 +348,10 @@ def execute(self) -> None:
 
         # Create $sourcebase/Makefile.am.
         for_test = True
-        directory = joinpath(self.testdir, sourcebase)
+        directory = os.path.join(self.testdir, sourcebase)
         if not os.path.isdir(directory):
             os.mkdir(directory)
-        destfile = joinpath(directory, 'Makefile.am')
+        destfile = os.path.join(directory, 'Makefile.am')
         if single_configure:
             emit = self.emitter.lib_Makefile_am(destfile, main_modules,
                                                 moduletable, self.makefiletable, '', for_test)
@@ -363,10 +362,10 @@ def execute(self) -> None:
             file.write(emit)
 
         # Create $m4base/Makefile.am.
-        directory = joinpath(self.testdir, m4base)
+        directory = os.path.join(self.testdir, m4base)
         if not os.path.isdir(directory):
             os.mkdir(directory)
-        destfile = joinpath(directory, 'Makefile.am')
+        destfile = os.path.join(directory, 'Makefile.am')
         emit = '## Process this file with automake to produce Makefile.in.\n\n'
         emit += 'EXTRA_DIST =\n'
         for file in filetable.all_files:
@@ -381,12 +380,12 @@ def execute(self) -> None:
 
         inctests = self.config.checkInclTestCategory(TESTS['tests'])
         if inctests:
-            directory = joinpath(self.testdir, testsbase)
+            directory = os.path.join(self.testdir, testsbase)
             if not os.path.isdir(directory):
                 os.mkdir(directory)
             if single_configure:
                 # Create $testsbase/Makefile.am.
-                destfile = joinpath(directory, 'Makefile.am')
+                destfile = os.path.join(directory, 'Makefile.am')
                 witness_macro = '%stests_WITNESS' % macro_prefix
                 emit = self.emitter.tests_Makefile_am(destfile, tests_modules, moduletable,
                                                       self.makefiletable, witness_macro, for_test)
@@ -394,7 +393,7 @@ def execute(self) -> None:
                     file.write(emit)
             else:  # if not single_configure
                 # Create $testsbase/Makefile.am.
-                destfile = joinpath(directory, 'Makefile.am')
+                destfile = os.path.join(directory, 'Makefile.am')
                 libtests = False
                 self.config.setLibtests(False)
                 emit = self.emitter.tests_Makefile_am(destfile, modules, moduletable,
@@ -404,7 +403,7 @@ def execute(self) -> None:
                 # Viewed from the $testsbase subdirectory, $auxdir is different.
                 emit = ''
                 saved_auxdir = auxdir
-                auxdir = os.path.normpath(joinpath(relinverse(testsbase), auxdir))
+                auxdir = os.path.normpath(os.path.join(relinverse(testsbase), auxdir))
                 self.config.setAuxDir(auxdir)
                 # Create $testsbase/configure.ac.
                 emit += '# Process this file with autoconf '
@@ -490,7 +489,7 @@ def execute(self) -> None:
                 emit += 'AH_TOP([#include \"../config.h\"])\n\n'
                 emit += 'AC_CONFIG_FILES([Makefile])\n'
                 emit += 'AC_OUTPUT\n'
-                path = joinpath(self.testdir, testsbase, 'configure.ac')
+                path = os.path.join(self.testdir, testsbase, 'configure.ac')
                 with open(path, mode='w', newline='\n', encoding='utf-8') as file:
                     file.write(emit)
 
@@ -506,7 +505,7 @@ def execute(self) -> None:
         emit += 'AUTOMAKE_OPTIONS = 1.14 foreign\n\n'
         emit += 'SUBDIRS = %s\n\n' % ' '.join(subdirs)
         emit += 'ACLOCAL_AMFLAGS = -I %s\n' % m4base
-        path = joinpath(self.testdir, 'Makefile.am')
+        path = os.path.join(self.testdir, 'Makefile.am')
         with open(path, mode='w', newline='\n', encoding='utf-8') as file:
             file.write(emit)
 
@@ -626,12 +625,12 @@ def execute(self) -> None:
         for directory in subdirs:
             # For subdirs that have a configure.ac by their own, it's the subdir's
             # configure.ac which creates the subdir's Makefile.am, not this one.
-            makefiles.append(joinpath(directory, 'Makefile'))
+            makefiles.append(os.path.join(directory, 'Makefile'))
         if not single_configure:
             makefiles = makefiles[:-1]
         emit += 'AC_CONFIG_FILES([%s])\n' % ' '.join(makefiles)
         emit += 'AC_OUTPUT\n'
-        path = joinpath(self.testdir, 'configure.ac')
+        path = os.path.join(self.testdir, 'configure.ac')
         with open(path, mode='w', newline='\n', encoding='utf-8') as file:
             file.write(emit)
 
@@ -641,11 +640,11 @@ def execute(self) -> None:
         force_output()
         os.chdir(self.testdir)
         # gettext
-        if os.path.isfile(joinpath(m4base, 'gettext.m4')):
+        if os.path.isfile(os.path.join(m4base, 'gettext.m4')):
             args = [UTILS['autopoint'], '--force']
             execute(args, verbose)
             for src in os.listdir(m4base):
-                src = joinpath(m4base, src)
+                src = os.path.join(m4base, src)
                 if src.endswith('.m4~'):
                     dest = src[:-1]
                     if os.path.isfile(dest):
@@ -678,22 +677,22 @@ def execute(self) -> None:
         if inctests and not single_configure:
             # Do not use "${AUTORECONF} --force --install", because it may invoke
             # autopoint, which brings in older versions of some of our .m4 files.
-            os.chdir(joinpath(self.testdir, testsbase))
+            os.chdir(os.path.join(self.testdir, testsbase))
             # gettext
-            if os.path.isfile(joinpath(m4base, 'gettext.m4')):
+            if os.path.isfile(os.path.join(m4base, 'gettext.m4')):
                 args = [UTILS['autopoint'], '--force']
                 execute(args, verbose)
                 for src in os.listdir(m4base):
-                    src = joinpath(m4base, src)
+                    src = os.path.join(m4base, src)
                     if src.endswith('.m4~'):
                         dest = src[:-1]
                         if os.path.isfile(dest):
                             os.remove(dest)
                         movefile(src, dest)
             # aclocal
-            args = [UTILS['aclocal'], '-I', joinpath('..', m4base)]
+            args = [UTILS['aclocal'], '-I', os.path.join('..', m4base)]
             execute(args, verbose)
-            if not os.path.isdir(joinpath('../build-aux')):
+            if not os.path.isdir(os.path.join('../build-aux')):
                 print('executing mkdir ../build-aux')
                 os.mkdir('../build-aux')
             # autoconf
@@ -713,7 +712,7 @@ def execute(self) -> None:
 
         # Need to run configure and make once, to create built files that are to be
         # distributed (such as parse-datetime.c).
-        path = joinpath(self.testdir, sourcebase, 'Makefile.am')
+        path = os.path.join(self.testdir, sourcebase, 'Makefile.am')
         with open(path, mode='r', newline='\n', encoding='utf-8') as file:
             snippet = file.read()
         snippet = combine_lines(snippet)
@@ -758,7 +757,7 @@ def execute(self) -> None:
         tests_distributed_built_sources = []
         if inctests:
             # Likewise for built files in the $testsbase directory.
-            path = joinpath(self.testdir, testsbase, 'Makefile.am')
+            path = os.path.join(self.testdir, testsbase, 'Makefile.am')
             with open(path, mode='r', newline='\n', encoding='utf-8') as file:
                 snippet = file.read()
             snippet = combine_lines(snippet)
@@ -840,7 +839,7 @@ def execute(self) -> None:
                     'LIBTOOLIZE=%s' % UTILS['libtoolize'],
                     'distclean']
             sp.call(args)
-        if os.path.isfile(joinpath('build-aux', 'test-driver')):
+        if os.path.isfile(os.path.join('build-aux', 'test-driver')):
             _patch_test_driver()
         os.chdir(DIRS['cwd'])
 
@@ -899,7 +898,7 @@ def execute(self) -> None:
         # First, all modules one by one.
         for module in modules:
             self.config.setModules([module.name])
-            GLTestDir(self.config, joinpath(self.megatestdir, module.name)).execute()
+            GLTestDir(self.config, os.path.join(self.megatestdir, module.name)).execute()
             megasubdirs.append(module.name)
 
         # Then, all modules all together.
@@ -909,7 +908,7 @@ def execute(self) -> None:
                     if module.name != 'config-h' ]
         self.config.setModules([ module.name
                                  for module in modules ])
-        GLTestDir(self.config, joinpath(self.megatestdir, 'ALL')).execute()
+        GLTestDir(self.config, os.path.join(self.megatestdir, 'ALL')).execute()
         megasubdirs.append('ALL')
 
         # Create autobuild.
@@ -927,10 +926,10 @@ def execute(self) -> None:
         repdict['Oct'] = repdict['October'] = '10'
         repdict['Nov'] = repdict['November'] = '11'
         repdict['Dec'] = repdict['December'] = '12'
-        vc_witness = joinpath(DIRS['root'], '.git', 'refs', 'heads', 'master')
+        vc_witness = os.path.join(DIRS['root'], '.git', 'refs', 'heads', 'master')
         if not os.path.isfile(vc_witness):
-            vc_witness = joinpath(DIRS['root'], 'ChangeLog')
-        mdate_sh = joinpath(DIRS['root'], 'build-aux', 'mdate-sh')
+            vc_witness = os.path.join(DIRS['root'], 'ChangeLog')
+        mdate_sh = os.path.join(DIRS['root'], 'build-aux', 'mdate-sh')
         args = ['sh', mdate_sh, vc_witness]
         cvsdate = sp.check_output(args).decode('UTF-8').strip()
         for key in repdict:
@@ -962,7 +961,7 @@ def execute(self) -> None:
         emit += '  ) 2>&1 | { if test -n "$AUTOBUILD_SUBST"; then '
         emit += 'sed -e "$AUTOBUILD_SUBST"; else cat; fi; } > logs/$safemodule\n'
         emit += 'done\n'
-        path = joinpath(self.megatestdir, 'do-autobuild')
+        path = os.path.join(self.megatestdir, 'do-autobuild')
         with open(path, mode='w', newline='\n', encoding='utf-8') as file:
             file.write(emit)
 
@@ -971,7 +970,7 @@ def execute(self) -> None:
         emit += 'AUTOMAKE_OPTIONS = 1.14 foreign\n\n'
         emit += 'SUBDIRS = %s\n\n' % ' '.join(megasubdirs)
         emit += 'EXTRA_DIST = do-autobuild\n'
-        path = joinpath(self.megatestdir, 'Makefile.am')
+        path = os.path.join(self.megatestdir, 'Makefile.am')
         with open(path, mode='w', newline='\n', encoding='utf-8') as file:
             file.write(emit)
 
@@ -985,7 +984,7 @@ def execute(self) -> None:
         emit += 'AC_CONFIG_SUBDIRS([%s])\n' % ' '.join(megasubdirs)
         emit += 'AC_CONFIG_FILES([Makefile])\n'
         emit += 'AC_OUTPUT\n'
-        path = joinpath(self.megatestdir, 'configure.ac')
+        path = os.path.join(self.megatestdir, 'configure.ac')
         with open(path, mode='w', newline='\n', encoding='utf-8') as file:
             file.write(emit)
 
@@ -1005,6 +1004,6 @@ def execute(self) -> None:
         args = [UTILS['automake'], '--add-missing', '--copy']
         execute(args, verbose)
         rmtree('autom4te.cache')
-        if os.path.isfile(joinpath('build-aux', 'test-driver')):
+        if os.path.isfile(os.path.join('build-aux', 'test-driver')):
             _patch_test_driver()
         os.chdir(DIRS['cwd'])
diff --git a/pygnulib/constants.py b/pygnulib/constants.py
index 71811576da..29952f4f46 100644
--- a/pygnulib/constants.py
+++ b/pygnulib/constants.py
@@ -252,20 +252,6 @@ def cleaner(sequence: str | list[str]) -> str | list[str | bool]:
     return sequence
 
 
-def joinpath(head: str, *tail: str) -> str:
-    '''Join two or more pathname components, inserting '/' as needed. If any
-    component is an absolute path, all previous path components will be
-    discarded.
-    This function also replaces SUBDIR/../ with empty; therefore it is not
-    suitable when some of the pathname components use Makefile variables
-    such as '$(srcdir)'.'''
-    newtail = []
-    for item in tail:
-        newtail.append(item)
-    result = os.path.normpath(os.path.join(head, *tail))
-    return result
-
-
 def relativize(dir1: str, dir2: str) -> str:
     '''Compute a relative pathname reldir such that dir1/reldir = dir2.
     dir1 and dir2 must be relative pathnames.'''
@@ -276,7 +262,7 @@ def relativize(dir1: str, dir2: str) -> str:
         first = dir1[:dir1.find(os.path.sep)]
         if first != '.':
             if first == '..':
-                dir2 = joinpath(os.path.basename(dir0), dir2)
+                dir2 = os.path.join(os.path.basename(dir0), dir2)
                 dir0 = os.path.dirname(dir0)
             else:  # if first != '..'
                 # Get first component of dir2
@@ -284,8 +270,8 @@ def relativize(dir1: str, dir2: str) -> str:
                 if first == first2:
                     dir2 = dir2[dir2.find(os.path.sep) + 1:]
                 else:  # if first != first2
-                    dir2 = joinpath('..', dir2)
-                dir0 = joinpath(dir0, first)
+                    dir2 = os.path.join('..', dir2)
+                dir0 = os.path.join(dir0, first)
         dir1 = dir1[dir1.find(os.path.sep) + 1:]
     result = os.path.normpath(dir2)
     return result
@@ -369,7 +355,7 @@ def symlink_relative(src: str, dest: str) -> None:
             # src is relative to the directory of dest.
             last_slash = dest.rfind('/')
             if last_slash >= 0:
-                cp_src = joinpath(dest[0:last_slash-1], src)
+                cp_src = os.path.join(dest[0:last_slash-1], src)
             else:
                 cp_src = src
         copyfile2(cp_src, dest)
@@ -388,7 +374,7 @@ def as_link_value_at_dest(src: str, dest: str) -> str:
         return src
     else:  # if src is not absolute
         if os.path.isabs(dest):
-            return joinpath(os.getcwd(), src)
+            return os.path.join(os.getcwd(), src)
         else:  # if dest is not absolute
             destdir = os.path.dirname(dest)
             if not destdir:
@@ -433,7 +419,7 @@ def hardlink(src: str, dest: str) -> None:
             # src is relative to the directory of dest.
             last_slash = dest.rfind('/')
             if last_slash >= 0:
-                cp_src = joinpath(dest[0: last_slash - 1], src)
+                cp_src = os.path.join(dest[0: last_slash - 1], src)
             else:
                 cp_src = src
         copyfile2(cp_src, dest)
diff --git a/pygnulib/main.py b/pygnulib/main.py
index c1c2cd2c1e..aa678afd25 100644
--- a/pygnulib/main.py
+++ b/pygnulib/main.py
@@ -92,7 +92,6 @@
     UTILS,
     MODES,
     ENCS,
-    joinpath,
     lines_to_multiline,
     ensure_writable,
     copyfile,
@@ -859,9 +858,9 @@ def main(temp_directory: str) -> None:
     elif mode == 'find':
         modulesystem = GLModuleSystem(config)
         for filename in files:
-            if (os.path.isfile(joinpath(DIRS['root'], filename))
+            if (os.path.isfile(os.path.join(DIRS['root'], filename))
                     or (localpath != None
-                        and any([ os.path.isfile(joinpath(localdir, filename))
+                        and any([ os.path.isfile(os.path.join(localdir, filename))
                                   for localdir in localpath ]))):
                 # Convert the file name to a POSIX basic regex.
                 # Needs to handle . [ \ * ^ $.
@@ -956,7 +955,7 @@ def main(temp_directory: str) -> None:
             if m4base:
                 # Apply func_import to a particular gnulib directory.
                 # Any number of additional modules can be given.
-                if not os.path.isfile(joinpath(destdir, m4base, 'gnulib-cache.m4')):
+                if not os.path.isfile(os.path.join(destdir, m4base, 'gnulib-cache.m4')):
                     # First use of gnulib in the given m4base.
                     if not sourcebase:
                         sourcebase = 'lib'
@@ -983,7 +982,7 @@ def main(temp_directory: str) -> None:
                 # too expensive.)
                 m4dirs = []
                 dirisnext = False
-                filepath = joinpath(destdir, 'Makefile.am')
+                filepath = os.path.join(destdir, 'Makefile.am')
                 if os.path.isfile(filepath):
                     with open(filepath, mode='r', newline='\n', encoding='utf-8') as file:
                         data = file.read()
@@ -997,7 +996,7 @@ def main(temp_directory: str) -> None:
                         if dirisnext:
                             # Ignore absolute directory pathnames, like /usr/local/share/aclocal.
                             if not os.path.isabs(aclocal_amflag):
-                                if os.path.isfile(joinpath(destdir, aclocal_amflag, 'gnulib-cache.m4')):
+                                if os.path.isfile(os.path.join(destdir, aclocal_amflag, 'gnulib-cache.m4')):
                                     m4dirs.append(aclocal_amflag)
                             dirisnext = False
                         else:  # if not dirisnext
@@ -1008,11 +1007,11 @@ def main(temp_directory: str) -> None:
                     for arg in guessed_m4dirs:
                         # Ignore absolute directory pathnames, like /usr/local/share/aclocal.
                         if not os.path.isabs(arg):
-                            if os.path.isfile(joinpath(destdir, arg, 'gnulib-cache.m4')):
+                            if os.path.isfile(os.path.join(destdir, arg, 'gnulib-cache.m4')):
                                 m4dirs.append(arg)
                 else:  # if not os.path.isfile(filepath)
                     # No Makefile.am! Oh well. Look at the last generated aclocal.m4.
-                    filepath = joinpath(destdir, 'aclocal.m4')
+                    filepath = os.path.join(destdir, 'aclocal.m4')
                     if os.path.isfile(filepath):
                         pattern = re.compile(r'm4_include\(\[(.*?)]\)')
                         with open(filepath, mode='r', newline='\n', encoding='utf-8') as file:
@@ -1021,7 +1020,7 @@ def main(temp_directory: str) -> None:
                                    for m4dir in m4dirs ]
                         m4dirs = [ m4dir
                                    for m4dir in m4dirs
-                                   if os.path.isfile(joinpath(destdir, m4dir, 'gnulib-cache.m4')) ]
+                                   if os.path.isfile(os.path.join(destdir, m4dir, 'gnulib-cache.m4')) ]
                         m4dirs = sorted(set(m4dirs))
                 if len(m4dirs) == 0:
                     # First use of gnulib in a package.
@@ -1320,7 +1319,7 @@ def main(temp_directory: str) -> None:
             destdir = os.path.dirname(dest)
             destpath = os.path.basename(dest)
         # Create the directory for destfile.
-        dirname = os.path.dirname(joinpath(destdir, destpath))
+        dirname = os.path.dirname(os.path.join(destdir, destpath))
         if not config['dryrun']:
             if dirname and not os.path.isdir(dirname):
                 try:  # Try to create directories
@@ -1335,10 +1334,10 @@ def main(temp_directory: str) -> None:
         ensure_writable(tmpfile)
         assistant.setOriginal(srcpath)
         assistant.setRewritten(destpath)
-        if os.path.isfile(joinpath(destdir, destpath)):
+        if os.path.isfile(os.path.join(destdir, destpath)):
             # The file already exists.
             assistant.update(lookedup, flag, tmpfile, True)
-        else:  # if not os.path.isfile(joinpath(destdir, destpath))
+        else:  # if not os.path.isfile(os.path.join(destdir, destpath))
             # Install the file.
             # Don't protest if the file should be there but isn't: it happens
             # frequently that developers don't put autogenerated files under
@@ -1359,7 +1358,7 @@ def main(temp_directory: str) -> None:
         # This disturbs the result of the next "gitk" invocation.
         # Workaround: Let git scan the files. This can be done through
         # "git update-index --refresh" or "git status" or "git diff".
-        if os.path.isdir(joinpath(DIRS['root'], '.git')):
+        if os.path.isdir(os.path.join(DIRS['root'], '.git')):
             try:
                 sp.run(['git', 'update-index', '--refresh'],
                        cwd=DIRS['root'], stdout=sp.DEVNULL)
-- 
2.45.2

Reply via email to