Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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])),
]
Expand Down
56 changes: 0 additions & 56 deletions Sources/PackLib/Foundation+Utils.swift

This file was deleted.

1 change: 1 addition & 0 deletions Sources/PackLib/PackSchema.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Yams
import XUtils

public struct PackSchemaBase: Codable, Sendable {
public enum Version: Int, Codable, Sendable {
Expand Down
1 change: 1 addition & 0 deletions Sources/PackLib/Packer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import XUtils

public struct Packer: Sendable {
public let buildSettings: BuildSettings
Expand Down
3 changes: 2 additions & 1 deletion Sources/PackLib/Planner.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import XUtils

public struct Planner: Sendable {
public var buildSettings: BuildSettings
Expand Down Expand Up @@ -213,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
Expand Down
15 changes: 5 additions & 10 deletions Sources/XKit/Integration/IntegratedInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import SwiftyMobileDevice
import ConcurrencyExtras
import Dependencies
import XUtils

extension LockdownClient {
static let installerLabel = "xtool"
Expand Down Expand Up @@ -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<Task<Void, Never>?>(nil)
Expand Down Expand Up @@ -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":
Expand All @@ -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 }

Expand Down
9 changes: 3 additions & 6 deletions Sources/XToolSupport/DevCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ArgumentParser
import PackLib
import XKit
import Dependencies
import XUtils

struct PackOperation {
struct BuildOptions: ParsableArguments {
Expand Down Expand Up @@ -136,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 {
Expand Down
1 change: 1 addition & 0 deletions Sources/XToolSupport/SDKCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Version
import ArgumentParser
import Dependencies
import PackLib
import XUtils

struct SDKCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
Expand Down
2 changes: 2 additions & 0 deletions Sources/XToolSupport/XTool.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Expand Down
25 changes: 25 additions & 0 deletions Sources/XUtils/Foundation+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

extension Data {
// AsyncBytes is Darwin-only :/

package init(reading fileHandle: FileHandle) async throws {
#if canImport(Darwin)
self = try await fileHandle.bytes.reduce(into: Data()) { $0.append($1) }
#else
self = try fileHandle.readToEnd() ?? Data()
#endif
}

package init(reading file: URL) async throws {
#if canImport(Darwin)
self = try await file.resourceBytes.reduce(into: Data()) { $0.append($1) }
#else
try self.init(contentsOf: file)
#endif
}
}

package func stderrPrint(_ message: String, terminator: String = "\n") {
try? FileHandle.standardError.write(contentsOf: Data("\(message)\(terminator)".utf8))
}
97 changes: 97 additions & 0 deletions Sources/XUtils/TemporaryDirectory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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

/// Prepares a fresh tmpdir root.
///
/// Optional, but try calling this at launch to clean up old resources.
package static func prepare() {
_ = 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: "_")
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<URL, Errors>
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)"
}
}
}
}