Skip to content

Create helper functions for temporary directories which supports new concurrency features #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions Sources/TSCBasic/TemporaryFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private extension TempFileError {
/// - dir: If present this will be the temporary directory.
///
/// - Returns: Path to directory in which temporary file should be created.
/// - Throws: `TempFileError`
public func determineTempDirectory(_ dir: AbsolutePath? = nil) throws -> AbsolutePath {
let tmpDir = dir ?? localFileSystem.tempDirectory
guard localFileSystem.isDirectory(tmpDir) else {
Expand Down Expand Up @@ -231,22 +232,11 @@ extension MakeDirectoryError: CustomNSError {
/// return value for the `withTemporaryDirectory` function.
/// The cleanup block should be called when the temporary directory is no longer needed.
///
/// - Throws: MakeDirectoryError and rethrows all errors from `body`.
/// - Throws: `MakeDirectoryError` and rethrows all errors from `body`.
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory" , _ body: (AbsolutePath, @escaping (AbsolutePath) -> Void) throws -> Result
) throws -> Result {
// Construct path to the temporary directory.
let templatePath = try AbsolutePath(prefix + ".XXXXXX", relativeTo: determineTempDirectory(dir))

// Convert templatePath to a C style string terminating with null char to be an valid input
// to mkdtemp method. The XXXXXX in this string will be replaced by a random string
// which will be the actual path to the temporary directory.
var template = [UInt8](templatePath.pathString.utf8).map({ Int8($0) }) + [Int8(0)]

if TSCLibc.mkdtemp(&template) == nil {
throw MakeDirectoryError(errno: errno)
}

let template = try createTemporaryDirectoryTemplate(dir: dir, prefix: prefix)
return try body(AbsolutePath(String(cString: template))) { path in
_ = try? FileManager.default.removeItem(atPath: path.pathString)
}
Expand All @@ -267,7 +257,7 @@ public func withTemporaryDirectory<Result>(
/// If `body` has a return value, that value is also used as the
/// return value for the `withTemporaryDirectory` function.
///
/// - Throws: MakeDirectoryError and rethrows all errors from `body`.
/// - Throws: `MakeDirectoryError` and rethrows all errors from `body`.
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false , _ body: (AbsolutePath) throws -> Result
) throws -> Result {
Expand All @@ -277,3 +267,70 @@ public func withTemporaryDirectory<Result>(
}
}

/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
/// The temporary directory will live on disk while the closure is evaluated and will be deleted when
/// the cleanup closure is called. This allows the temporary directory to have an arbitrary lifetime.
///
/// This function is basically a wrapper over posix's mkdtemp() function.
///
/// - Parameters:
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
/// variables are set, dir will be set to `/tmp/`.
/// - prefix: The prefix to the temporary file name.
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
/// If `body` has a return value, that value is also used as the
/// return value for the `withTemporaryDirectory` function.
/// The cleanup block should be called when the temporary directory is no longer needed.
///
/// - Throws: `MakeDirectoryError` and rethrows all errors from `body`.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory" , _ body: (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result
) async throws -> Result {
let template = try createTemporaryDirectoryTemplate(dir: dir, prefix: prefix)
return try await body(AbsolutePath(String(cString: template))) { path in
_ = try? FileManager.default.removeItem(atPath: path.pathString)
}
}

/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
/// The temporary directory will live on disk while the closure is evaluated and will be deleted afterwards.
///
/// This function is basically a wrapper over posix's mkdtemp() function.
///
/// - Parameters:
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
/// variables are set, dir will be set to `/tmp/`.
/// - prefix: The prefix to the temporary file name.
/// - removeTreeOnDeinit: If enabled try to delete the whole directory tree otherwise remove only if its empty.
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
/// If `body` has a return value, that value is also used as the
/// return value for the `withTemporaryDirectory` function.
///
/// - Throws: MakeDirectoryError and rethrows all errors from `body`.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false , _ body: (AbsolutePath) async throws -> Result
) async throws -> Result {
try await withTemporaryDirectory(dir: dir, prefix: prefix) { path, cleanup in
defer { if removeTreeOnDeinit { cleanup(path) } }
return try await body(path)
}
}

private func createTemporaryDirectoryTemplate(dir: AbsolutePath?, prefix: String) throws -> [Int8] {
// Construct path to the temporary directory.
let templatePath = try AbsolutePath(prefix + ".XXXXXX", relativeTo: determineTempDirectory(dir))

// Convert templatePath to a C style string terminating with null char to be an valid input
// to mkdtemp method. The XXXXXX in this string will be replaced by a random string
// which will be the actual path to the temporary directory.
var template = [UInt8](templatePath.pathString.utf8).map({ Int8($0) }) + [Int8(0)]

if TSCLibc.mkdtemp(&template) == nil {
throw MakeDirectoryError(errno: errno)
}
return template
}
73 changes: 73 additions & 0 deletions Tests/TSCBasicTests/TemporaryFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,76 @@ class TemporaryFileTests: XCTestCase {
#endif
}
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
class TemporaryAsyncFileTests: XCTestCase {
func testBasicTemporaryDirectory() async throws {
// Test can create and remove temp directory.
let path1: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
// Do some async task
let task = Task {
return
}
await task.value

XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
return tempDirPath
}
XCTAssertFalse(localFileSystem.isDirectory(path1))

// Test temp directory is not removed when its not empty.
let path2: AbsolutePath = try await withTemporaryDirectory { tempDirPath in
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
// Create a file inside the temp directory.
let filePath = tempDirPath.appending(component: "somefile")
// Do some async task
let task = Task {
return
}
await task.value

try localFileSystem.writeFileContents(filePath, bytes: ByteString())
return tempDirPath
}
XCTAssertTrue(localFileSystem.isDirectory(path2))
// Cleanup.
try FileManager.default.removeItem(atPath: path2.pathString)
XCTAssertFalse(localFileSystem.isDirectory(path2))

// Test temp directory is removed when its not empty and removeTreeOnDeinit is enabled.
let path3: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
let filePath = tempDirPath.appending(component: "somefile")
// Do some async task
let task = Task {
return
}
await task.value

try localFileSystem.writeFileContents(filePath, bytes: ByteString())
return tempDirPath
}
XCTAssertFalse(localFileSystem.isDirectory(path3))
}

func testCanCreateUniqueTempDirectories() async throws {
let (pathOne, pathTwo): (AbsolutePath, AbsolutePath) = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathOne in
let pathTwo: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathTwo in
// Do some async task
let task = Task {
return
}
await task.value

XCTAssertTrue(localFileSystem.isDirectory(pathOne))
XCTAssertTrue(localFileSystem.isDirectory(pathTwo))
// Their paths should be different.
XCTAssertTrue(pathOne != pathTwo)
return pathTwo
}
return (pathOne, pathTwo)
}
XCTAssertFalse(localFileSystem.isDirectory(pathOne))
XCTAssertFalse(localFileSystem.isDirectory(pathTwo))
}
}