From bbcce89292d96eafc87b86c0ced3fc4ff01e56f1 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 17:47:51 -0400 Subject: [PATCH 1/5] Create XUtils --- Package.swift | 4 ++++ Sources/PackLib/PackSchema.swift | 1 + Sources/PackLib/Packer.swift | 1 + Sources/PackLib/Planner.swift | 1 + Sources/XToolSupport/DevCommand.swift | 1 + Sources/{PackLib => XUtils}/Foundation+Utils.swift | 0 6 files changed, 8 insertions(+) rename Sources/{PackLib => XUtils}/Foundation+Utils.swift (100%) diff --git a/Package.swift b/Package.swift index 4a702eec..a1e26ff0 100644 --- a/Package.swift +++ b/Package.swift @@ -90,11 +90,14 @@ let package = Package( ], exclude: ["openapi-generator-config.yaml", "patch.js"] ), + // common utilities shared across xtool targets + .target(name: "XUtils"), .target( name: "XKit", dependencies: [ "DeveloperAPI", "CXKit", + "XUtils", .byName(name: "XADI", condition: .when(platforms: [.linux])), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), @@ -165,6 +168,7 @@ let package = Package( .target( name: "PackLib", dependencies: [ + "XUtils", .product(name: "Yams", package: "Yams"), .product(name: "XcodeGenKit", package: "XcodeGen", condition: .when(platforms: [.macOS])), ] diff --git a/Sources/PackLib/PackSchema.swift b/Sources/PackLib/PackSchema.swift index e696d2d6..406ef24a 100644 --- a/Sources/PackLib/PackSchema.swift +++ b/Sources/PackLib/PackSchema.swift @@ -1,5 +1,6 @@ import Foundation import Yams +import XUtils public struct PackSchemaBase: Codable, Sendable { public enum Version: Int, Codable, Sendable { diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index b8523437..ff402bdf 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -1,4 +1,5 @@ import Foundation +import XUtils public struct Packer: Sendable { public let buildSettings: BuildSettings diff --git a/Sources/PackLib/Planner.swift b/Sources/PackLib/Planner.swift index cf564305..505755f4 100644 --- a/Sources/PackLib/Planner.swift +++ b/Sources/PackLib/Planner.swift @@ -1,4 +1,5 @@ import Foundation +import XUtils public struct Planner: Sendable { public var buildSettings: BuildSettings diff --git a/Sources/XToolSupport/DevCommand.swift b/Sources/XToolSupport/DevCommand.swift index d2a57ffe..b699ebda 100644 --- a/Sources/XToolSupport/DevCommand.swift +++ b/Sources/XToolSupport/DevCommand.swift @@ -3,6 +3,7 @@ import ArgumentParser import PackLib import XKit import Dependencies +import XUtils struct PackOperation { struct BuildOptions: ParsableArguments { diff --git a/Sources/PackLib/Foundation+Utils.swift b/Sources/XUtils/Foundation+Utils.swift similarity index 100% rename from Sources/PackLib/Foundation+Utils.swift rename to Sources/XUtils/Foundation+Utils.swift From 368ec00c8ee92a9de2e2b75a9115bb63729d0096 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 17:57:17 -0400 Subject: [PATCH 2/5] 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 505755f4..b3f3e93b 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 1a558196..a8cb990c 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 5a1073d2..50d02dfb 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 00000000..c83533dc --- /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)" + } + } + } +} From 576973692aafc6f5a838f87182cf7cfbc47d134e Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 18:05:19 -0400 Subject: [PATCH 3/5] Prepare at launch --- Sources/XToolSupport/XTool.swift | 2 ++ Sources/XUtils/TemporaryDirectory.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Sources/XToolSupport/XTool.swift b/Sources/XToolSupport/XTool.swift index d4d0a416..b369dd6e 100644 --- a/Sources/XToolSupport/XTool.swift +++ b/Sources/XToolSupport/XTool.swift @@ -1,9 +1,11 @@ import Foundation import XKit import ArgumentParser +import XUtils public enum XTool { public static func run(arguments: [String]? = nil) async throws { + TemporaryDirectory.prepare() await XToolCommand.cancellableMain(arguments) } } diff --git a/Sources/XUtils/TemporaryDirectory.swift b/Sources/XUtils/TemporaryDirectory.swift index c83533dc..9873124e 100644 --- a/Sources/XUtils/TemporaryDirectory.swift +++ b/Sources/XUtils/TemporaryDirectory.swift @@ -6,6 +6,13 @@ package struct TemporaryDirectory: ~Copyable { private var shouldDelete: Bool package let url: URL + /// Prepares a fresh tmpdir root. + /// + /// Optional, but try calling this at launch to clean up old resources. + package static func prepare() { + _ = TemporaryDirectoryRoot.shared + } + package init(name: String) throws { do { let basename = name.replacingOccurrences(of: ".", with: "_") From 4262bfa077428c813e014d7823938ee1bcceb19c Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 18:14:34 -0400 Subject: [PATCH 4/5] small tweaks --- Sources/XToolSupport/DevCommand.swift | 8 ++------ Sources/XUtils/TemporaryDirectory.swift | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/XToolSupport/DevCommand.swift b/Sources/XToolSupport/DevCommand.swift index b699ebda..83f8ac65 100644 --- a/Sources/XToolSupport/DevCommand.swift +++ b/Sources/XToolSupport/DevCommand.swift @@ -137,12 +137,8 @@ struct DevBuildCommand: AsyncParsableCommand { if ipa { @Dependency(\.zipCompressor) var compressor finalURL = url.deletingPathExtension().appendingPathExtension("ipa") - let tmpDir = try TemporaryDirectory(name: "sh.xtool.tmp") - let payloadDir = tmpDir.url.appendingPathComponent("Payload", isDirectory: true) - try FileManager.default.createDirectory( - at: payloadDir, - withIntermediateDirectories: true - ) + let tmpDir = try TemporaryDirectory(name: "Payload") + let payloadDir = tmpDir.url try FileManager.default.moveItem(at: url, to: payloadDir.appendingPathComponent(url.lastPathComponent)) let ipaURL = try await compressor.compress(directory: payloadDir) { progress in if let progress { diff --git a/Sources/XUtils/TemporaryDirectory.swift b/Sources/XUtils/TemporaryDirectory.swift index 9873124e..6f50a303 100644 --- a/Sources/XUtils/TemporaryDirectory.swift +++ b/Sources/XUtils/TemporaryDirectory.swift @@ -13,6 +13,10 @@ package struct TemporaryDirectory: ~Copyable { _ = TemporaryDirectoryRoot.shared } + /// Creates a temporary directory where `lastPathComponent` is exactly `name`. + /// + /// The directory is deleted on deinit or (if the object never deinits) on next launch. + /// To save the contents, move them elsewhere with ``persist(at:)``. package init(name: String) throws { do { let basename = name.replacingOccurrences(of: ".", with: "_") From be1c7e4e1e116fec0597bfe5cf07485fc4232ee7 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 27 Jul 2025 18:41:03 -0400 Subject: [PATCH 5/5] Fix Linux --- Sources/XToolSupport/SDKCommand.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/XToolSupport/SDKCommand.swift b/Sources/XToolSupport/SDKCommand.swift index ebb75c67..d587ec81 100644 --- a/Sources/XToolSupport/SDKCommand.swift +++ b/Sources/XToolSupport/SDKCommand.swift @@ -4,6 +4,7 @@ import Version import ArgumentParser import Dependencies import PackLib +import XUtils struct SDKCommand: AsyncParsableCommand { static let configuration = CommandConfiguration(