From f33568a4c90819508bf334c4cb511eb25ff22fc1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 3 Nov 2025 11:39:48 -0500 Subject: [PATCH 1/4] Capture the current working directory early on OpenBSD. This PR adds some OpenBSD-specific code to capture the current working directory as early as possible. We then use that directory when argv[0] appears to be a relative path so that the following incantation works correctly: ```sh ./.build/debug/myPackageTests.xctest --testing-library swift-testing ``` This logic is not necessary on other platforms because they all provide a way to get the path to the current executable. OpenBSD has no such API. --- .../Additions/CommandLineAdditions.swift | 32 +++++++++++++++-- Sources/_TestingInternals/include/Stubs.h | 35 +++++++++++++++++++ Tests/TestingTests/ExitTestTests.swift | 8 +++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 57d9851a8..ca71c3a49 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -10,6 +10,17 @@ private import _TestingInternals +#if os(OpenBSD) +/// At process start (before `main()` is called), capture the current working +/// directory. +/// +/// -Note: This function is the only valid caller of `swt_captureEarlyCWD()`. +@_section(".init_array.101") @_used +private let _captureEarlyCWD: @convention(c) () -> Void = { + swt_captureEarlyCWD() +} +#endif + extension CommandLine { /// The path to the current process' executable. static var executablePath: String { @@ -33,6 +44,15 @@ extension CommandLine { } return result! #elseif os(Linux) || os(Android) + guard var argv0 = arguments.first, argv0.contains("/") else { + throw CError(rawValue: ENOEXEC) + } + if argv0.first != "/", + let earlyCWD = _earlyCWD.flatMap(String.init(validatingCString:)), + !earlyCWD.isEmpty { + argv0 = "\(earlyCWD)/\(argv0)" + } + return argv0 return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX) * 2) { buffer in let readCount = readlink("/proc/self/exe", buffer.baseAddress!, buffer.count - 1) guard readCount >= 0 else { @@ -57,11 +77,17 @@ extension CommandLine { } #elseif os(OpenBSD) // OpenBSD does not have API to get a path to the running executable. Use - // arguments[0]. We do a basic sniff test for a path-like string, but - // otherwise return argv[0] verbatim. - guard let argv0 = arguments.first, argv0.contains("/") else { + // arguments[0]. We do a basic sniff test for a path-like string, and + // prepend the early CWD if it looks like a relative path, but otherwise + // return argv[0] verbatim. + guard var argv0 = arguments.first, argv0.contains("/") else { throw CError(rawValue: ENOEXEC) } + if argv0.first != "/", + let earlyCWD = swt_getEarlyCWD().flatMap(String.init(validatingCString:)), + !earlyCWD.isEmpty { + argv0 = "\(earlyCWD)/\(argv0)" + } return argv0 #elseif os(Windows) var result: String? diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..6da5d2b1f 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -180,6 +180,41 @@ static int swt_setfdflags(int fd, int flags) { } #endif +#if defined(__OpenBSD__) +/// Storage for the early working directory. +static _Atomic(const char *_Nullable) swt_earlyCWD = NULL; + +/// At process start (before `main()` is called), capture the current working +/// directory. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +/// +/// - Important: Do not call this function. It is automatically called by the +/// loader when the process starts. To get the early current working +/// directory, call ``swt_getEarlyCWD()`` instead. +static void swt_captureEarlyCWD(void) { + static char buffer[PATH_MAX * 2]; + if (getcwd(buffer, sizeof(buffer))) { + // stdatomic.h is missing on OpenBSD 7.7, so use clang's builtins instead. + const char *expectingNULL = NULL; + __c11_atomic_store(&swt_earlyCWD, buffer, __ATOMIC_SEQ_CST); + } +} + +/// Get the current working directory as it was set shortly after the process +/// started and before `main()` has been called. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +static const char *_Nullable swt_getEarlyCWD(void) { + // stdatomic.h is missing on OpenBSD 7.7, so use clang's builtins instead. + return __c11_atomic_load(&swt_earlyCWD, __ATOMIC_SEQ_CST); +} +#endif + SWT_ASSUME_NONNULL_END #endif diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 5be229266..6edabc305 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -625,6 +625,14 @@ private import _TestingInternals } } #endif + +#if os(OpenBSD) + @Test("Changing the CWD doesn't break exit tests") + func changeCWD() async throws { + try #require(0 == chdir("/")) + await #expect(processExitsWith: .success) {} + } +#endif } // MARK: - Fixtures From 46dbf6a521bb83938fbc9143db9af60163cb7a27 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 3 Nov 2025 13:26:06 -0500 Subject: [PATCH 2/4] Accidentally pasted into the Linux section at some point --- .../Testing/Support/Additions/CommandLineAdditions.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index ca71c3a49..05850fb23 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -44,15 +44,6 @@ extension CommandLine { } return result! #elseif os(Linux) || os(Android) - guard var argv0 = arguments.first, argv0.contains("/") else { - throw CError(rawValue: ENOEXEC) - } - if argv0.first != "/", - let earlyCWD = _earlyCWD.flatMap(String.init(validatingCString:)), - !earlyCWD.isEmpty { - argv0 = "\(earlyCWD)/\(argv0)" - } - return argv0 return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX) * 2) { buffer in let readCount = readlink("/proc/self/exe", buffer.baseAddress!, buffer.count - 1) guard readCount >= 0 else { From 8e83e44697723c6252468e0e6d3b75db57a7db08 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 14:56:48 -0500 Subject: [PATCH 3/4] Lower the constructor function to C++ --- .../Additions/CommandLineAdditions.swift | 11 ------ Sources/_TestingInternals/CMakeLists.txt | 1 + Sources/_TestingInternals/ExecutablePath.cpp | 35 +++++++++++++++++++ .../include/ExecutablePath.h | 30 ++++++++++++++++ Sources/_TestingInternals/include/Stubs.h | 35 ------------------- 5 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 Sources/_TestingInternals/ExecutablePath.cpp create mode 100644 Sources/_TestingInternals/include/ExecutablePath.h diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 05850fb23..63decd1ad 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -10,17 +10,6 @@ private import _TestingInternals -#if os(OpenBSD) -/// At process start (before `main()` is called), capture the current working -/// directory. -/// -/// -Note: This function is the only valid caller of `swt_captureEarlyCWD()`. -@_section(".init_array.101") @_used -private let _captureEarlyCWD: @convention(c) () -> Void = { - swt_captureEarlyCWD() -} -#endif - extension CommandLine { /// The path to the current process' executable. static var executablePath: String { diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index a951c7d4b..b2bc5b6b1 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -12,6 +12,7 @@ include(GitCommit) include(TargetTriple) add_library(_TestingInternals STATIC Discovery.cpp + ExecutablePath.cpp Versions.cpp WillThrow.cpp) target_include_directories(_TestingInternals PUBLIC diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp new file mode 100644 index 000000000..0d23653f6 --- /dev/null +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -0,0 +1,35 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#include "ExecutablePath.h" + +#include + +#if defined(__OpenBSD__) +static std::atomic earlyCWD { nullptr }; + +/// At process start (before `main()` is called), capture the current working +/// directory. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +__attribute__((__constructor__(101), __used__)) +static void swt_captureEarlyCWD(void) { + static char buffer[PATH_MAX * 2]; + if (getcwd(buffer, sizeof(buffer))) { + earlyCWD.store(buffer); + } +} + +const char *swt_getEarlyCWD(void) { + return earlyCWD.load(); +} +#endif diff --git a/Sources/_TestingInternals/include/ExecutablePath.h b/Sources/_TestingInternals/include/ExecutablePath.h new file mode 100644 index 000000000..4c3c06800 --- /dev/null +++ b/Sources/_TestingInternals/include/ExecutablePath.h @@ -0,0 +1,30 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if !defined(SWT_EXECUTABLE_PATH_H) +#define SWT_EXECUTABLE_PATH_H + +#include "Defines.h" +#include "Includes.h" + +SWT_ASSUME_NONNULL_BEGIN + +#if defined(__OpenBSD__) +/// Get the executable path of the current process. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +SWT_EXTERN const char *_Nullable swt_getExecutablePath(void); +#endif + +SWT_ASSUME_NONNULL_END + +#endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 6da5d2b1f..636ea9aff 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -180,41 +180,6 @@ static int swt_setfdflags(int fd, int flags) { } #endif -#if defined(__OpenBSD__) -/// Storage for the early working directory. -static _Atomic(const char *_Nullable) swt_earlyCWD = NULL; - -/// At process start (before `main()` is called), capture the current working -/// directory. -/// -/// This function is necessary on OpenBSD so that we can (as correctly as -/// possible) resolve the executable path when the first argument is a relative -/// path (which can occur when manually invoking the test executable.) -/// -/// - Important: Do not call this function. It is automatically called by the -/// loader when the process starts. To get the early current working -/// directory, call ``swt_getEarlyCWD()`` instead. -static void swt_captureEarlyCWD(void) { - static char buffer[PATH_MAX * 2]; - if (getcwd(buffer, sizeof(buffer))) { - // stdatomic.h is missing on OpenBSD 7.7, so use clang's builtins instead. - const char *expectingNULL = NULL; - __c11_atomic_store(&swt_earlyCWD, buffer, __ATOMIC_SEQ_CST); - } -} - -/// Get the current working directory as it was set shortly after the process -/// started and before `main()` has been called. -/// -/// This function is necessary on OpenBSD so that we can (as correctly as -/// possible) resolve the executable path when the first argument is a relative -/// path (which can occur when manually invoking the test executable.) -static const char *_Nullable swt_getEarlyCWD(void) { - // stdatomic.h is missing on OpenBSD 7.7, so use clang's builtins instead. - return __c11_atomic_load(&swt_earlyCWD, __ATOMIC_SEQ_CST); -} -#endif - SWT_ASSUME_NONNULL_END #endif From 00e1630db49e2fe1d9c08e8e5963c790e8635d38 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 15:01:28 -0500 Subject: [PATCH 4/4] I did not rename that function, senator --- Sources/_TestingInternals/include/ExecutablePath.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/_TestingInternals/include/ExecutablePath.h b/Sources/_TestingInternals/include/ExecutablePath.h index 4c3c06800..22f3acc62 100644 --- a/Sources/_TestingInternals/include/ExecutablePath.h +++ b/Sources/_TestingInternals/include/ExecutablePath.h @@ -17,12 +17,13 @@ SWT_ASSUME_NONNULL_BEGIN #if defined(__OpenBSD__) -/// Get the executable path of the current process. +/// Get the current working directory as it was set shortly after the process +/// started and before `main()` has been called. /// /// This function is necessary on OpenBSD so that we can (as correctly as /// possible) resolve the executable path when the first argument is a relative /// path (which can occur when manually invoking the test executable.) -SWT_EXTERN const char *_Nullable swt_getExecutablePath(void); +SWT_EXTERN const char *_Nullable swt_getEarlyCWD(void); #endif SWT_ASSUME_NONNULL_END