diff --git a/Sources/tart/Commands/Clone.swift b/Sources/tart/Commands/Clone.swift index 0b38850..b6497e2 100644 --- a/Sources/tart/Commands/Clone.swift +++ b/Sources/tart/Commands/Clone.swift @@ -45,8 +45,8 @@ struct Clone: AsyncParsableCommand { } func run() async throws { - let ociStorage = VMStorageOCI() - let localStorage = VMStorageLocal() + let ociStorage = try VMStorageOCI() + let localStorage = try VMStorageLocal() if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) { // Pull the VM in case it's OCI-based and doesn't exist locally yet diff --git a/Sources/tart/Commands/Import.swift b/Sources/tart/Commands/Import.swift index 645b5bd..edb0253 100644 --- a/Sources/tart/Commands/Import.swift +++ b/Sources/tart/Commands/Import.swift @@ -17,7 +17,7 @@ struct Import: AsyncParsableCommand { } func run() async throws { - let localStorage = VMStorageLocal() + let localStorage = try VMStorageLocal() // Create a temporary VM directory to which we will load the export file let tmpVMDir = try VMDirectory.temporary() diff --git a/Sources/tart/Commands/Prune.swift b/Sources/tart/Commands/Prune.swift index 5e41e03..68f9bfa 100644 --- a/Sources/tart/Commands/Prune.swift +++ b/Sources/tart/Commands/Prune.swift @@ -53,9 +53,9 @@ struct Prune: AsyncParsableCommand { switch entries { case "caches": - prunableStorages = [VMStorageOCI(), try IPSWCache()] + prunableStorages = [try VMStorageOCI(), try IPSWCache()] case "vms": - prunableStorages = [VMStorageLocal()] + prunableStorages = [try VMStorageLocal()] default: throw ValidationError("unsupported --entries value, please specify either \"caches\" or \"vms\"") } @@ -152,7 +152,7 @@ struct Prune: AsyncParsableCommand { let transaction = SentrySDK.startTransaction(name: "Pruning cache", operation: "prune", bindToScope: true) defer { transaction.finish() } - let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] + let prunableStorages: [PrunableStorage] = [try VMStorageOCI(), try IPSWCache()] let prunables: [Prunable] = try prunableStorages .flatMap { try $0.prunables() } .sorted { try $0.accessDate() < $1.accessDate() } diff --git a/Sources/tart/Commands/Pull.swift b/Sources/tart/Commands/Pull.swift index 02c5e29..0d66658 100644 --- a/Sources/tart/Commands/Pull.swift +++ b/Sources/tart/Commands/Pull.swift @@ -35,7 +35,7 @@ struct Pull: AsyncParsableCommand { func run() async throws { // Be more liberal when accepting local image as argument, // see https://github.com/cirruslabs/tart/issues/36 - if VMStorageLocal().exists(remoteName) { + if try VMStorageLocal().exists(remoteName) { print("\"\(remoteName)\" is a local image, nothing to pull here!") return diff --git a/Sources/tart/Commands/Push.swift b/Sources/tart/Commands/Push.swift index 79ede79..5b48b21 100644 --- a/Sources/tart/Commands/Push.swift +++ b/Sources/tart/Commands/Push.swift @@ -39,7 +39,7 @@ struct Push: AsyncParsableCommand { var populateCache: Bool = false func run() async throws { - let ociStorage = VMStorageOCI() + let ociStorage = try VMStorageOCI() let localVMDir = try VMStorageHelper.open(localName) let lock = try localVMDir.lock() if try !lock.trylock() { diff --git a/Sources/tart/Commands/Rename.swift b/Sources/tart/Commands/Rename.swift index 66ad9bd..194b031 100644 --- a/Sources/tart/Commands/Rename.swift +++ b/Sources/tart/Commands/Rename.swift @@ -17,7 +17,7 @@ struct Rename: AsyncParsableCommand { } func run() async throws { - let localStorage = VMStorageLocal() + let localStorage = try VMStorageLocal() if !localStorage.exists(name) { throw ValidationError("failed to rename a non-existent local VM: \(name)") diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index fd7a851..52831a4 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -301,7 +301,7 @@ struct Run: AsyncParsableCommand { } } - let localStorage = VMStorageLocal() + let localStorage = try VMStorageLocal() let vmDir = try localStorage.open(name) if try vmDir.state() == .Suspended { suspendable = true @@ -334,7 +334,7 @@ struct Run: AsyncParsableCommand { @MainActor func run() async throws { - let localStorage = VMStorageLocal() + let localStorage = try VMStorageLocal() let vmDir = try localStorage.open(name) // Validate disk format support diff --git a/Sources/tart/Config.swift b/Sources/tart/Config.swift index 05b295f..64af7de 100644 --- a/Sources/tart/Config.swift +++ b/Sources/tart/Config.swift @@ -9,7 +9,8 @@ struct Config { var tartHomeDir: URL if let customTartHome = ProcessInfo.processInfo.environment["TART_HOME"] { - tartHomeDir = URL(fileURLWithPath: customTartHome) + tartHomeDir = URL(fileURLWithPath: customTartHome, isDirectory: true) + try Self.validateTartHome(url: tartHomeDir) } else { tartHomeDir = FileManager.default .homeDirectoryForCurrentUser @@ -49,4 +50,24 @@ struct Config { static func jsonDecoder() -> JSONDecoder { JSONDecoder() } + + private static func validateTartHome(url: URL) throws { + let urlComponents = url.pathComponents + + let descendingURLs = urlComponents.indices.map { i in + URL(fileURLWithPath: urlComponents[0...i].joined(separator: "/")) + } + + for descendingURL in descendingURLs { + if FileManager.default.fileExists(atPath: descendingURL.path) { + continue + } + + do { + try FileManager.default.createDirectory(at: descendingURL, withIntermediateDirectories: false) + } catch { + throw RuntimeError.Generic("TART_HOME is invalid: \(descendingURL.path) does not exist, yet we can't create it: \(error.localizedDescription)") + } + } + } } diff --git a/Sources/tart/Root.swift b/Sources/tart/Root.swift index 43a5721..477e6f9 100644 --- a/Sources/tart/Root.swift +++ b/Sources/tart/Root.swift @@ -92,7 +92,7 @@ struct Root: AsyncParsableCommand { do { try Config().gc() } catch { - fputs("Failed to perform garbage collection!\n\(error)\n", stderr) + fputs("Failed to perform garbage collection: \(error)\n", stderr) } } diff --git a/Sources/tart/VMStorageLocal.swift b/Sources/tart/VMStorageLocal.swift index 47b5b27..6a39420 100644 --- a/Sources/tart/VMStorageLocal.swift +++ b/Sources/tart/VMStorageLocal.swift @@ -1,7 +1,11 @@ import Foundation class VMStorageLocal: PrunableStorage { - let baseURL: URL = try! Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true) + let baseURL: URL + + init() throws { + baseURL = try Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true) + } private func vmURL(_ name: String) -> URL { baseURL.appendingPathComponent(name, isDirectory: true) diff --git a/Sources/tart/VMStorageOCI.swift b/Sources/tart/VMStorageOCI.swift index f71c547..a92b748 100644 --- a/Sources/tart/VMStorageOCI.swift +++ b/Sources/tart/VMStorageOCI.swift @@ -3,7 +3,11 @@ import Sentry import Retry class VMStorageOCI: PrunableStorage { - let baseURL = try! Config().tartCacheDir.appendingPathComponent("OCIs", isDirectory: true) + let baseURL: URL + + init() throws { + baseURL = try Config().tartCacheDir.appendingPathComponent("OCIs", isDirectory: true) + } private func vmURL(_ name: RemoteName) -> URL { baseURL.appendingRemoteName(name)