diff --git a/Sources/Basics/Archiver/TarArchiver.swift b/Sources/Basics/Archiver/TarArchiver.swift index c0bf27cb788..2d1c7fce5b2 100644 --- a/Sources/Basics/Archiver/TarArchiver.swift +++ b/Sources/Basics/Archiver/TarArchiver.swift @@ -25,7 +25,7 @@ public struct TarArchiver: Archiver { private let cancellator: Cancellator /// The underlying command - private let tarCommand: String + internal let tarCommand: String /// Creates a `TarArchiver`. /// diff --git a/Sources/Basics/Archiver/ZipArchiver.swift b/Sources/Basics/Archiver/ZipArchiver.swift index cf1a8b7f38f..d4f4f1bb7d2 100644 --- a/Sources/Basics/Archiver/ZipArchiver.swift +++ b/Sources/Basics/Archiver/ZipArchiver.swift @@ -29,7 +29,14 @@ public struct ZipArchiver: Archiver, Cancellable { /// Absolute path to the Windows tar in the system folder #if os(Windows) - private let windowsTar: String + internal let windowsTar: String + #else + internal let unzip = "unzip" + internal let zip = "zip" + #endif + + #if os(FreeBSD) + internal let tar = "tar" #endif /// Creates a `ZipArchiver`. @@ -74,7 +81,9 @@ public struct ZipArchiver: Archiver, Cancellable { // It's part of system32 anyway so use the absolute path. let process = AsyncProcess(arguments: [windowsTar, "xf", archivePath.pathString, "-C", destinationPath.pathString]) #else - let process = AsyncProcess(arguments: ["unzip", archivePath.pathString, "-d", destinationPath.pathString]) + let process = AsyncProcess(arguments: [ + self.unzip, archivePath.pathString, "-d", destinationPath.pathString, + ]) #endif guard let registrationKey = self.cancellator.register(process) else { throw CancellationError.failedToRegisterProcess(process) @@ -113,7 +122,10 @@ public struct ZipArchiver: Archiver, Cancellable { // On FreeBSD, the unzip command is available in base but not the zip command. // Therefore; we use libarchive(bsdtar) to produce the ZIP archive instead. let process = AsyncProcess( - arguments: ["tar", "-c", "--format", "zip", "-f", destinationPath.pathString, directory.basename], + arguments: [ + self.tar, "-c", "--format", "zip", "-f", destinationPath.pathString, + directory.basename, + ], workingDirectory: directory.parentDirectory ) #else @@ -127,7 +139,7 @@ public struct ZipArchiver: Archiver, Cancellable { arguments: [ "/bin/sh", "-c", - "cd \(directory.parentDirectory.underlying.pathString) && zip -ry \(destinationPath.pathString) \(directory.basename)", + "cd \(directory.parentDirectory.underlying.pathString) && \(self.zip) -ry \(destinationPath.pathString) \(directory.basename)" ] ) #endif @@ -154,7 +166,7 @@ public struct ZipArchiver: Archiver, Cancellable { #if os(Windows) let process = AsyncProcess(arguments: [windowsTar, "tf", path.pathString]) #else - let process = AsyncProcess(arguments: ["unzip", "-t", path.pathString]) + let process = AsyncProcess(arguments: [self.unzip, "-t", path.pathString]) #endif guard let registrationKey = self.cancellator.register(process) else { throw CancellationError.failedToRegisterProcess(process) diff --git a/Sources/_InternalTestSupport/XCTAssertHelpers.swift b/Sources/_InternalTestSupport/XCTAssertHelpers.swift index 83506d0463b..33ad7eac963 100644 --- a/Sources/_InternalTestSupport/XCTAssertHelpers.swift +++ b/Sources/_InternalTestSupport/XCTAssertHelpers.swift @@ -76,6 +76,31 @@ public func XCTSkipOnWindows(because reason: String? = nil, skipPlatformCi: Bool #endif } +public func _requiresTools(_ executable: String) throws { + func getAsyncProcessArgs(_ executable: String) -> [String] { + #if os(Windows) + let args = ["cmd.exe", "/c", "where.exe", executable] + #else + let args = ["which", executable] + #endif + return args + } + try AsyncProcess.checkNonZeroExit(arguments: getAsyncProcessArgs(executable)) +} +public func XCTRequires( + executable: String, + file: StaticString = #filePath, + line: UInt = #line +) throws { + + do { + try _requiresTools(executable) + } catch (let AsyncProcessResult.Error.nonZeroExit(result)) { + throw XCTSkip( + "Skipping as tool \(executable) is not found in the path. (\(result.description))") + } +} + /// An `async`-friendly replacement for `XCTAssertThrowsError`. public func XCTAssertAsyncThrowsError( _ expression: @autoclosure () async throws -> T, diff --git a/Tests/BasicsTests/Archiver/TarArchiverTests.swift b/Tests/BasicsTests/Archiver/TarArchiverTests.swift index b330e4599c9..b4ab64025df 100644 --- a/Tests/BasicsTests/Archiver/TarArchiverTests.swift +++ b/Tests/BasicsTests/Archiver/TarArchiverTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +@testable import struct Basics.TarArchiver import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported import _InternalTestSupport import XCTest @@ -18,6 +19,11 @@ import XCTest import struct TSCBasic.FileSystemError final class TarArchiverTests: XCTestCase { + override func setUp() async throws { + let archiver = TarArchiver(fileSystem: localFileSystem) + try XCTRequires(executable: archiver.tarCommand) + } + func testSuccess() async throws { try await testWithTemporaryDirectory { tmpdir in let archiver = TarArchiver(fileSystem: localFileSystem) diff --git a/Tests/BasicsTests/Archiver/UniversalArchiverTests.swift b/Tests/BasicsTests/Archiver/UniversalArchiverTests.swift index 69e6bb76911..451dd35a651 100644 --- a/Tests/BasicsTests/Archiver/UniversalArchiverTests.swift +++ b/Tests/BasicsTests/Archiver/UniversalArchiverTests.swift @@ -11,6 +11,8 @@ //===----------------------------------------------------------------------===// import Basics +@testable import struct Basics.TarArchiver +@testable import struct Basics.ZipArchiver import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported import _InternalTestSupport import XCTest @@ -18,6 +20,22 @@ import XCTest import struct TSCBasic.FileSystemError final class UniversalArchiverTests: XCTestCase { + override func setUp() async throws { + let zipAchiver = ZipArchiver(fileSystem: localFileSystem) + #if os(Windows) + try XCTRequires(executable: zipAchiver.windowsTar) + #else + try XCTRequires(executable: zipAchiver.unzip) + try XCTRequires(executable: zipAchiver.zip) + #endif + #if os(FreeBSD) + try XCTRequires(executable: zipAchiver.tar) + #endif + + let tarAchiver = TarArchiver(fileSystem: localFileSystem) + try XCTRequires(executable: tarAchiver.tarCommand) + } + func testSuccess() async throws { try await testWithTemporaryDirectory { tmpdir in let archiver = UniversalArchiver(localFileSystem) diff --git a/Tests/BasicsTests/Archiver/ZipArchiverTests.swift b/Tests/BasicsTests/Archiver/ZipArchiverTests.swift index 7b036813376..a9666ab17cc 100644 --- a/Tests/BasicsTests/Archiver/ZipArchiverTests.swift +++ b/Tests/BasicsTests/Archiver/ZipArchiverTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +@testable import struct Basics.ZipArchiver import _InternalTestSupport import XCTest import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported @@ -18,6 +19,19 @@ import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported import struct TSCBasic.FileSystemError final class ZipArchiverTests: XCTestCase { + override func setUp() async throws { + let archiver = ZipArchiver(fileSystem: localFileSystem) + #if os(Windows) + try XCTRequires(executable: archiver.windowsTar) + #else + try XCTRequires(executable: archiver.unzip) + try XCTRequires(executable: archiver.zip) + #endif + #if os(FreeBSD) + try XCTRequires(executable: archiver.tar) + #endif + } + func testZipArchiverSuccess() async throws { try await testWithTemporaryDirectory { tmpdir in let archiver = ZipArchiver(fileSystem: localFileSystem) diff --git a/Tests/_InternalTestSupportTests/Misc.swift b/Tests/_InternalTestSupportTests/Misc.swift index a88d8241128..1b44eaabe37 100644 --- a/Tests/_InternalTestSupportTests/Misc.swift +++ b/Tests/_InternalTestSupportTests/Misc.swift @@ -1,3 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// import SPMBuildCore import _InternalTestSupport import XCTest diff --git a/Tests/_InternalTestSupportTests/XCTAssertHelpersTests.swift b/Tests/_InternalTestSupportTests/XCTAssertHelpersTests.swift new file mode 100644 index 00000000000..0755356a898 --- /dev/null +++ b/Tests/_InternalTestSupportTests/XCTAssertHelpersTests.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import XCTest +import func _InternalTestSupport.XCTAssertThrows +import func _InternalTestSupport._requiresTools + +final class TestRequiresTool: XCTestCase { + func testErrorIsThrownIfExecutableIsNotFoundOnThePath() throws { + XCTAssertThrows( + try _requiresTools("doesNotExists") + ) { (error: AsyncProcessResult.Error) in + return true + } + } + + func testErrorIsNotThrownIfExecutableIsOnThePath() throws { + // Essentially call either "which which" or "where.exe where.exe" + #if os(Window) + let executable = "where.exe" + #else + let executable = "which" + #endif + XCTAssertNoThrow( + try _requiresTools(executable) + ) + } +} \ No newline at end of file