From 255ff13b88503a2aedbd1c13d3a8e675d7e71761 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 12:46:54 +0200 Subject: [PATCH 1/7] gh-136413: Add os.linkat() function --- Doc/library/os.rst | 63 ++++++++++++ Doc/whatsnew/3.15.rst | 8 ++ .../pycore_global_objects_fini_generated.h | 2 + Include/internal/pycore_global_strings.h | 2 + .../internal/pycore_runtime_init_generated.h | 2 + .../internal/pycore_unicodeobject_generated.h | 8 ++ Lib/test/test_os.py | 52 ++++++++++ ...-07-08-13-54-14.gh-issue-136413.imf6xb.rst | 4 + Modules/clinic/posixmodule.c.h | 98 ++++++++++++++++++- Modules/posixmodule.c | 57 +++++++++++ 10 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-08-13-54-14.gh-issue-136413.imf6xb.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 1e54cfec609bd2..d05a237a012a99 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2046,6 +2046,39 @@ features: If it's unavailable, using it will raise a :exc:`NotImplementedError`. +.. data:: AT_FDCWD + + .. availability:: Linux. + +.. data:: AT_EMPTY_PATH + + If *src_path* is an empty string, create a link to the file referenced by + *src_dir_fd* (which may have been obtained using the open(2) :data:`O_PATH` + flag). In this case, *src_dir_fd* can refer to any type of file except a + directory. This will generally not work if the file has a link count of + zero (files created with :data:`O_TMPFILE` and without :data:`O_EXCL` are an + exception). + + The caller must have the ``CAP_DAC_READ_SEARCH`` capability in order to use + this flag. + + .. availability:: Linux >= 2.6.39. + +.. data:: AT_SYMLINK_FOLLOW + + By default, :func:`linkat`, does not dereference *src_path* if it is a + symbolic link (like :func:`link`). The flag :data:`!AT_SYMLINK_FOLLOW` can + be specified in flags to cause *src_path* to be dereferenced if it is a + symbolic link. + + If procfs is mounted, this can be used as an alternative to + :data:`AT_EMPTY_PATH`, like this:: + + os.linkat(os.AT_FDCWD, "/proc/self/fd/", + dst_dir_fd, dst_name, os.AT_SYMLINK_FOLLOW) + + .. availability:: Linux >= 2.6.18. + .. function:: access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) @@ -2354,6 +2387,36 @@ features: Accepts a :term:`path-like object` for *src* and *dst*. +.. function:: linkat(src_dir_fd, src_path, dst_dir_fd, dst_path, flags, /) + + The :func:`!linkat` function operates in exactly the same way as + :func:`link`, except for the differences described here. + + If the pathname given in *src_path* is relative, then it is interpreted + relative to the directory referred to by the file descriptor *src_dir_fd* + (rather than relative to the current working directory of the calling + process, as is done by link() for a relative pathname). + + If *src_path* is relative and *src_dir_fd* is the special value + :data:`AT_FDCWD`, then *src_path* is interpreted relative to the current + working directory of the calling process (like :func:`link`). + + If *src_path* is absolute, then *src_dir_fd* is ignored. + + The interpretation of *dst_path* is as for *src_path*, except that a + relative pathname is interpreted relative to the directory referred to + by the file descriptor *dst_dir_fd*. + + The following values can be bitwise ORed in flags: + + * :data:`AT_EMPTY_PATH` + * :data:`AT_SYMLINK_FOLLOW` + + .. audit-event:: os.linkat src_dir_fd,src_path,dst_dir_fd,dst_path,flags os.linkat + + .. availability:: Unix. + + .. function:: listdir(path='.') Return a list containing the names of the entries in the directory given by diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 706a816f888b30..c958b079706e41 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -119,6 +119,14 @@ math (Contributed by Bénédikt Tran in :gh:`135853`.) +os +-- + +* Added :func:`os.linkat` to create a hard link with flags. + Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and + :data:`os.AT_SYMLINK_FOLLOW`. + (Contributed by Victor Stinner in :gh:`136413`.) + os.path ------- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index c461bc1786ddf4..74aed1e23bf691 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -919,6 +919,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dont_inherit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dst)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dst_dir_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dst_path)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(eager_start)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(effective_ids)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(element_factory)); @@ -1238,6 +1239,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(spam)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src_dir_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src_path)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stacklevel)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(start)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(statement)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 72c2051bd97660..c044c865e6013d 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -410,6 +410,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(dont_inherit) STRUCT_FOR_ID(dst) STRUCT_FOR_ID(dst_dir_fd) + STRUCT_FOR_ID(dst_path) STRUCT_FOR_ID(eager_start) STRUCT_FOR_ID(effective_ids) STRUCT_FOR_ID(element_factory) @@ -729,6 +730,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(spam) STRUCT_FOR_ID(src) STRUCT_FOR_ID(src_dir_fd) + STRUCT_FOR_ID(src_path) STRUCT_FOR_ID(stacklevel) STRUCT_FOR_ID(start) STRUCT_FOR_ID(statement) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index d378fcae26cf35..de2f972bd3b8e6 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -917,6 +917,7 @@ extern "C" { INIT_ID(dont_inherit), \ INIT_ID(dst), \ INIT_ID(dst_dir_fd), \ + INIT_ID(dst_path), \ INIT_ID(eager_start), \ INIT_ID(effective_ids), \ INIT_ID(element_factory), \ @@ -1236,6 +1237,7 @@ extern "C" { INIT_ID(spam), \ INIT_ID(src), \ INIT_ID(src_dir_fd), \ + INIT_ID(src_path), \ INIT_ID(stacklevel), \ INIT_ID(start), \ INIT_ID(statement), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index e516211f6c6cbc..1f116cab930972 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1428,6 +1428,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(dst_path); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(eager_start); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2704,6 +2708,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(src_path); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(stacklevel); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 5217037ae9d812..3d064793bc8cc9 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2655,6 +2655,58 @@ def test_unicode_name(self): self.file2 = self.file1 + "2" self._test_link(self.file1, self.file2) + +@unittest.skipUnless(hasattr(os, 'linkat'), 'requires os.linkat') +class LinkAtTests(unittest.TestCase): + def test_no_flags(self): + # create hard link with no flags + src = "linkat_src" + self.addCleanup(os_helper.unlink, src) + with open(src, "w", encoding='utf8') as fp: + fp.write("hello") + + dst = "linkat_dst" + self.addCleanup(os_helper.unlink, dst) + os.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 + + with open(dst, encoding='utf8') as fp: + self.assertEqual(fp.read(), 'hello') + + # destination already exists + src2 = "linkat_src2" + self.addCleanup(os_helper.unlink, src2) + with open(src2, "w", encoding='utf8') as fp: + fp.write("PYTHON") + + with self.assertRaises(FileExistsError): + os.linkat(os.AT_FDCWD, src2, os.AT_FDCWD, dst) # flags=0 + + def check_flag(self, flag): + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + + fd = os.open(os.path.curdir, os.O_WRONLY | os.O_TMPFILE, 0o600) + os.write(fd, b"hello") + if flag == os.AT_EMPTY_PATH: + os.linkat(fd, b"", + os.AT_FDCWD, filename, + os.AT_EMPTY_PATH) + else: + os.linkat(fd, f"/proc/self/fd/{fd}", + os.AT_FDCWD, filename, + os.AT_SYMLINK_FOLLOW) + os.close(fd) + + with open(filename, encoding='utf8') as fp: + self.assertEqual(fp.read(), 'hello') + + def test_empty_path(self): + self.check_flag(os.AT_EMPTY_PATH) + + def test_symlink_follow(self): + self.check_flag(os.AT_SYMLINK_FOLLOW) + + @unittest.skipIf(sys.platform == "win32", "Posix specific tests") class PosixUidGidTests(unittest.TestCase): # uid_t and gid_t are 32-bit unsigned integers on Linux diff --git a/Misc/NEWS.d/next/Library/2025-07-08-13-54-14.gh-issue-136413.imf6xb.rst b/Misc/NEWS.d/next/Library/2025-07-08-13-54-14.gh-issue-136413.imf6xb.rst new file mode 100644 index 00000000000000..6fc60cb5d31a6f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-08-13-54-14.gh-issue-136413.imf6xb.rst @@ -0,0 +1,4 @@ +Added :func:`os.linkat` to create a hard link with flags. Patch by Victor +Added also constants :data:`os.AT_EMPTY_PATH`, :data:`os.AT_FDCWD` and +:data:`os.AT_SYMLINK_FOLLOW`. +Stinner. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 3621a0625411d3..361dacdf160833 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -1580,6 +1580,98 @@ os_link(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwn #endif /* defined(HAVE_LINK) */ +#if defined(HAVE_LINK) + +PyDoc_STRVAR(os_linkat__doc__, +"linkat($module, /, src_dir_fd, src_path, dst_dir_fd, dst_path, flags=0)\n" +"--\n" +"\n" +"Create a hard link to a file."); + +#define OS_LINKAT_METHODDEF \ + {"linkat", _PyCFunction_CAST(os_linkat), METH_FASTCALL|METH_KEYWORDS, os_linkat__doc__}, + +static PyObject * +os_linkat_impl(PyObject *module, int src_dir_fd, path_t *src_path, + int dst_dir_fd, path_t *dst_path, int flags); + +static PyObject * +os_linkat(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 5 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(src_dir_fd), &_Py_ID(src_path), &_Py_ID(dst_dir_fd), &_Py_ID(dst_path), &_Py_ID(flags), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"src_dir_fd", "src_path", "dst_dir_fd", "dst_path", "flags", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "linkat", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[5]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 4; + int src_dir_fd = DEFAULT_DIR_FD; + path_t src_path = PATH_T_INITIALIZE_P("linkat", "src_path", 0, 0, 0, 0); + int dst_dir_fd = DEFAULT_DIR_FD; + path_t dst_path = PATH_T_INITIALIZE_P("linkat", "dst_path", 0, 0, 0, 0); + int flags = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 4, /*maxpos*/ 5, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!dir_fd_converter(args[0], &src_dir_fd)) { + goto exit; + } + if (!path_converter(args[1], &src_path)) { + goto exit; + } + if (!dir_fd_converter(args[2], &dst_dir_fd)) { + goto exit; + } + if (!path_converter(args[3], &dst_path)) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + flags = PyLong_AsInt(args[4]); + if (flags == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional_pos: + return_value = os_linkat_impl(module, src_dir_fd, &src_path, dst_dir_fd, &dst_path, flags); + +exit: + /* Cleanup for src_path */ + path_cleanup(&src_path); + /* Cleanup for dst_path */ + path_cleanup(&dst_path); + + return return_value; +} + +#endif /* defined(HAVE_LINK) */ + PyDoc_STRVAR(os_listdir__doc__, "listdir($module, /, path=None)\n" "--\n" @@ -12787,6 +12879,10 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored)) #define OS_LINK_METHODDEF #endif /* !defined(OS_LINK_METHODDEF) */ +#ifndef OS_LINKAT_METHODDEF + #define OS_LINKAT_METHODDEF +#endif /* !defined(OS_LINKAT_METHODDEF) */ + #ifndef OS_LISTDRIVES_METHODDEF #define OS_LISTDRIVES_METHODDEF #endif /* !defined(OS_LISTDRIVES_METHODDEF) */ @@ -13398,4 +13494,4 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */ -/*[clinic end generated code: output=ae64df0389746258 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=9e2feff720128973 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b570f81b7cf7c2..1fe3472ab1e6ec 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4448,6 +4448,52 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #endif +#ifdef HAVE_LINK +/*[clinic input] + +os.linkat + + src_dir_fd : dir_fd + src_path : path_t + dst_dir_fd : dir_fd + dst_path : path_t + flags : int = 0 + +Create a hard link to a file with flags. +[clinic start generated code]*/ + +static PyObject * +os_linkat_impl(PyObject *module, int src_dir_fd, path_t *src_path, + int dst_dir_fd, path_t *dst_path, int flags) +/*[clinic end generated code: output=8582ba3975d7ac3f input=f9b7d4b37ce271ef]*/ +{ + if (PySys_Audit("os.linkat", "iOiOi", + src_dir_fd, src_path->object, + dst_dir_fd, dst_path->object, + flags) < 0) { + return NULL; + } + + int result; + Py_BEGIN_ALLOW_THREADS + if (HAVE_LINKAT_RUNTIME) { + result = linkat(src_dir_fd, src_path->narrow, + dst_dir_fd, dst_path->narrow, + flags); + } + else { + result = ENOSYS; + } + Py_END_ALLOW_THREADS + + if (result) + return path_error2(src_path, dst_path); + + Py_RETURN_NONE; +} +#endif + + #if defined(MS_WINDOWS) && !defined(HAVE_OPENDIR) static PyObject * _listdir_windows_no_opendir(path_t *path, PyObject *list) @@ -16992,6 +17038,7 @@ static PyMethodDef posix_methods[] = { OS_GETCWD_METHODDEF OS_GETCWDB_METHODDEF OS_LINK_METHODDEF + OS_LINKAT_METHODDEF OS_LISTDIR_METHODDEF OS_LISTDRIVES_METHODDEF OS_LISTMOUNTS_METHODDEF @@ -17828,6 +17875,16 @@ all_ins(PyObject *m) if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR", LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)) return -1; #endif +#ifdef AT_FDCWD + if (PyModule_AddIntMacro(m, AT_FDCWD)) return -1; +#endif +#ifdef AT_EMPTY_PATH + if (PyModule_AddIntMacro(m, AT_EMPTY_PATH)) return -1; +#endif +#ifdef AT_SYMLINK_FOLLOW + if (PyModule_AddIntMacro(m, AT_SYMLINK_FOLLOW)) return -1; +#endif + return 0; } From 4b3f6d7f2554c3c8a8ca2ae896a4fd5b2a0f7554 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 14:28:36 +0200 Subject: [PATCH 2/7] Fix #ifdef HAVE_LINKAT --- Modules/posixmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 1fe3472ab1e6ec..cfd3b02671e413 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4448,7 +4448,7 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #endif -#ifdef HAVE_LINK +#ifdef HAVE_LINKAT /*[clinic input] os.linkat From e17b0f084aaf920339236deb882c9c318e8b5042 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 14:45:42 +0200 Subject: [PATCH 3/7] Skip tests if required flags are missing --- Lib/test/test_os.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 3d064793bc8cc9..b0b9aaf1de14a2 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2681,6 +2681,7 @@ def test_no_flags(self): with self.assertRaises(FileExistsError): os.linkat(os.AT_FDCWD, src2, os.AT_FDCWD, dst) # flags=0 + @unittest.skipUnless(hasattr(os, 'O_TMPFILE'), 'need os.O_TMPFILE') def check_flag(self, flag): filename = os_helper.TESTFN self.addCleanup(os_helper.unlink, filename) @@ -2700,9 +2701,13 @@ def check_flag(self, flag): with open(filename, encoding='utf8') as fp: self.assertEqual(fp.read(), 'hello') + @unittest.skipUnless(hasattr(os, 'AT_EMPTY_PATH'), + 'need os.AT_EMPTY_PATH') def test_empty_path(self): self.check_flag(os.AT_EMPTY_PATH) + @unittest.skipUnless(hasattr(os, 'AT_SYMLINK_FOLLOW'), + 'need os.AT_SYMLINK_FOLLOW') def test_symlink_follow(self): self.check_flag(os.AT_SYMLINK_FOLLOW) From d62d70293ed17a76516b4939a478494f4c7cbf48 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 14:50:20 +0200 Subject: [PATCH 4/7] Fix tests on WASI --- Lib/test/test_os.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index b0b9aaf1de14a2..0f62c65bc5c713 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2659,7 +2659,6 @@ def test_unicode_name(self): @unittest.skipUnless(hasattr(os, 'linkat'), 'requires os.linkat') class LinkAtTests(unittest.TestCase): def test_no_flags(self): - # create hard link with no flags src = "linkat_src" self.addCleanup(os_helper.unlink, src) with open(src, "w", encoding='utf8') as fp: @@ -2672,14 +2671,19 @@ def test_no_flags(self): with open(dst, encoding='utf8') as fp: self.assertEqual(fp.read(), 'hello') - # destination already exists - src2 = "linkat_src2" - self.addCleanup(os_helper.unlink, src2) - with open(src2, "w", encoding='utf8') as fp: - fp.write("PYTHON") + # linkat() fails with "OSError: [Errno 0]" on WASI + @unittest.skipIf(support.is_wasi, 'test broken on WASI') + def test_destination_exists(self): + src = "linkat_src" + self.addCleanup(os_helper.unlink, src) + open(src, "w").close() + + dst = "linkat_dst" + self.addCleanup(os_helper.unlink, dst) + open(dst, "w").close() with self.assertRaises(FileExistsError): - os.linkat(os.AT_FDCWD, src2, os.AT_FDCWD, dst) # flags=0 + os.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 @unittest.skipUnless(hasattr(os, 'O_TMPFILE'), 'need os.O_TMPFILE') def check_flag(self, flag): From ee54ca2e2d988c8b63f1ac5e432c3cbf8bc68ca4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 14:51:07 +0200 Subject: [PATCH 5/7] Run make clinic --- Modules/clinic/posixmodule.c.h | 8 ++++---- Modules/posixmodule.c | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 361dacdf160833..8d2525d5bd4d71 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -1580,13 +1580,13 @@ os_link(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwn #endif /* defined(HAVE_LINK) */ -#if defined(HAVE_LINK) +#if defined(HAVE_LINKAT) PyDoc_STRVAR(os_linkat__doc__, "linkat($module, /, src_dir_fd, src_path, dst_dir_fd, dst_path, flags=0)\n" "--\n" "\n" -"Create a hard link to a file."); +"Create a hard link to a file with flags."); #define OS_LINKAT_METHODDEF \ {"linkat", _PyCFunction_CAST(os_linkat), METH_FASTCALL|METH_KEYWORDS, os_linkat__doc__}, @@ -1670,7 +1670,7 @@ os_linkat(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *k return return_value; } -#endif /* defined(HAVE_LINK) */ +#endif /* defined(HAVE_LINKAT) */ PyDoc_STRVAR(os_listdir__doc__, "listdir($module, /, path=None)\n" @@ -13494,4 +13494,4 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */ -/*[clinic end generated code: output=9e2feff720128973 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3bf4693908acf071 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index cfd3b02671e413..fbf2d783549ed2 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4465,7 +4465,7 @@ Create a hard link to a file with flags. static PyObject * os_linkat_impl(PyObject *module, int src_dir_fd, path_t *src_path, int dst_dir_fd, path_t *dst_path, int flags) -/*[clinic end generated code: output=8582ba3975d7ac3f input=f9b7d4b37ce271ef]*/ +/*[clinic end generated code: output=8582ba3975d7ac3f input=82fe7b9292aa5e10]*/ { if (PySys_Audit("os.linkat", "iOiOi", src_dir_fd, src_path->object, From 5964acf8c8b418a43d97e380633e82b0f9f2ba9e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 15:04:19 +0200 Subject: [PATCH 6/7] Fix HAVE_LINKAT_RUNTIME --- Modules/posixmodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index fbf2d783549ed2..ff51903b2a68f7 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4482,7 +4482,8 @@ os_linkat_impl(PyObject *module, int src_dir_fd, path_t *src_path, flags); } else { - result = ENOSYS; + errno = ENOSYS; + result = -1; } Py_END_ALLOW_THREADS From e3e2e00b08aec4df97df2ef9ea6521ab53407b23 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Jul 2025 15:20:08 +0200 Subject: [PATCH 7/7] Try to fix tests on WASI --- Lib/test/test_os.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 0f62c65bc5c713..5b7873ed35068e 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2658,6 +2658,15 @@ def test_unicode_name(self): @unittest.skipUnless(hasattr(os, 'linkat'), 'requires os.linkat') class LinkAtTests(unittest.TestCase): + @staticmethod + def linkat(*args): + try: + os.linkat(*args) + except OSError as exc: + if exc.errno == errno.ENOSYS: + self.skipTest(str(exc)) + raise + def test_no_flags(self): src = "linkat_src" self.addCleanup(os_helper.unlink, src) @@ -2666,13 +2675,11 @@ def test_no_flags(self): dst = "linkat_dst" self.addCleanup(os_helper.unlink, dst) - os.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 + self.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 with open(dst, encoding='utf8') as fp: self.assertEqual(fp.read(), 'hello') - # linkat() fails with "OSError: [Errno 0]" on WASI - @unittest.skipIf(support.is_wasi, 'test broken on WASI') def test_destination_exists(self): src = "linkat_src" self.addCleanup(os_helper.unlink, src) @@ -2683,7 +2690,7 @@ def test_destination_exists(self): open(dst, "w").close() with self.assertRaises(FileExistsError): - os.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 + self.linkat(os.AT_FDCWD, src, os.AT_FDCWD, dst) # flags=0 @unittest.skipUnless(hasattr(os, 'O_TMPFILE'), 'need os.O_TMPFILE') def check_flag(self, flag):