From 368ec00c8ee92a9de2e2b75a9115bb63729d0096 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 17:57:17 -0400 Subject: [PATCH] TemporaryDirectoryRoot --- Sources/PackLib/Planner.swift | 2 +- .../Integration/IntegratedInstaller.swift | 15 ++-- Sources/XUtils/Foundation+Utils.swift | 31 ------- Sources/XUtils/TemporaryDirectory.swift | 86 +++++++++++++++++++ 4 files changed, 92 insertions(+), 42 deletions(-) create mode 100644 Sources/XUtils/TemporaryDirectory.swift diff --git a/Sources/PackLib/Planner.swift b/Sources/PackLib/Planner.swift index 505755f..b3f3e93 100644 --- a/Sources/PackLib/Planner.swift +++ b/Sources/PackLib/Planner.swift @@ -214,7 +214,7 @@ public struct Planner: Sendable { } private func dumpDependencies() async throws -> PackageDependency { - let tempDir = try TemporaryDirectory(name: "xtool-dump-\(UUID().uuidString)") + let tempDir = try TemporaryDirectory(name: "xtool-dump") let tempFileURL = tempDir.url.appendingPathComponent("dump.json") // SwiftPM sometimes prints extraneous data to stdout, so ask diff --git a/Sources/XKit/Integration/IntegratedInstaller.swift b/Sources/XKit/Integration/IntegratedInstaller.swift index 1a55819..a8cb990 100644 --- a/Sources/XKit/Integration/IntegratedInstaller.swift +++ b/Sources/XKit/Integration/IntegratedInstaller.swift @@ -2,6 +2,7 @@ import Foundation import SwiftyMobileDevice import ConcurrencyExtras import Dependencies +import XUtils extension LockdownClient { static let installerLabel = "xtool" @@ -54,9 +55,6 @@ public actor IntegratedInstaller { private var appInstaller: AppInstaller? - private let tempDir = FileManager.default.temporaryDirectoryShim - .appendingPathComponent("sh.xtool.Staging") - private var stage: String? private nonisolated let updateTask = LockIsolated?>(nil) @@ -206,12 +204,9 @@ public actor IntegratedInstaller { public func install(app: URL) async throws -> String { try await self.updateStage(to: "Unpacking app", initialProgress: nil) - if FileManager.default.fileExists(atPath: tempDir.path) { - try? FileManager.default.removeItem(at: tempDir) - } - - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: tempDir) } + let _tempDir = try TemporaryDirectory(name: "staging") + let tempDir = _tempDir.url + defer { withExtendedLifetime(_tempDir) {} } switch app.pathExtension { case "ipa": @@ -235,7 +230,7 @@ public actor IntegratedInstaller { try await self.updateProgress(to: 1) - let payload = self.tempDir.appendingPathComponent("Payload") + let payload = tempDir.appendingPathComponent("Payload") guard let appDir = payload.implicitContents.first(where: { $0.pathExtension == "app" }) else { throw Error.appExtractionFailed } diff --git a/Sources/XUtils/Foundation+Utils.swift b/Sources/XUtils/Foundation+Utils.swift index 5a1073d..50d02df 100644 --- a/Sources/XUtils/Foundation+Utils.swift +++ b/Sources/XUtils/Foundation+Utils.swift @@ -1,36 +1,5 @@ import Foundation -package struct TemporaryDirectory: ~Copyable { - private var shouldDelete = true - - package let url: URL - - package init(name: String) throws { - self.url = FileManager.default.temporaryDirectory.appendingPathComponent(name, isDirectory: true) - _delete() - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) - } - - private func _delete() { - try? FileManager.default.removeItem(at: url) - } - - package consuming func persist() -> URL { - shouldDelete = false - return url - } - - package consuming func persist(at location: URL) throws { - try FileManager.default.moveItem(at: url, to: location) - // we do this after moving, so that if the move fails we clean up - shouldDelete = false - } - - deinit { - if shouldDelete { _delete() } - } -} - extension Data { // AsyncBytes is Darwin-only :/ diff --git a/Sources/XUtils/TemporaryDirectory.swift b/Sources/XUtils/TemporaryDirectory.swift new file mode 100644 index 0000000..c83533d --- /dev/null +++ b/Sources/XUtils/TemporaryDirectory.swift @@ -0,0 +1,86 @@ +import Foundation + +package struct TemporaryDirectory: ~Copyable { + private static let debugTmp = ProcessInfo.processInfo.environment["XTL_DEBUG_TMP"] != nil + + private var shouldDelete: Bool + package let url: URL + + package init(name: String) throws { + do { + let basename = name.replacingOccurrences(of: ".", with: "_") + self.url = try TemporaryDirectoryRoot.shared.url + // ensures uniqueness + .appendingPathComponent("tmp-\(basename)-\(UUID().uuidString)") + .appendingPathComponent(name, isDirectory: true) + self.shouldDelete = true + } catch { + // non-copyable types can't be partially initialized so we need a stub value + self.url = URL(fileURLWithPath: "") + self.shouldDelete = false + throw error + } + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + + if Self.debugTmp { + stderrPrint("Created TemporaryDirectory: \(url.path)") + } + } + + private func _delete() { + guard !Self.debugTmp else { return } + try? FileManager.default.removeItem(at: url) + } + + package consuming func persist(at location: URL) throws { + try FileManager.default.moveItem(at: url, to: location) + // we do this after moving, so that if the move fails we clean up + shouldDelete = false + } + + deinit { + if shouldDelete { _delete() } + } +} + +private struct TemporaryDirectoryRoot { + static let shared = TemporaryDirectoryRoot() + + private let _url: Result + var url: URL { + get throws(Errors) { + try _url.get() + } + } + + private init() { + let base: URL + let env = ProcessInfo.processInfo.environment + if let tmpdir = env["XTL_TMPDIR"] ?? env["TMPDIR"] { + base = URL(fileURLWithPath: tmpdir) + } else { + base = FileManager.default.temporaryDirectory + } + + let url = base.appendingPathComponent("sh.xtool") + try? FileManager.default.removeItem(at: url) + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + } catch { + self._url = .failure(Errors.tmpdirCreationFailed(url, error)) + return + } + self._url = .success(url) + } + + enum Errors: Error, CustomStringConvertible { + case tmpdirCreationFailed(URL, Error) + + var description: String { + switch self { + case let .tmpdirCreationFailed(url, error): + "Could not create temporary directory at '\(url.path)': \(error)" + } + } + } +}