diff --git a/.swiftlint.yml b/.swiftlint.yml index e1a24a5d..5319f5b9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -44,13 +44,11 @@ deployment_target: tvOSApplicationExtension_deployment_target: 99 watchOS_deployment_target: 99 watchOSApplicationExtension_deployment_target: 99 -explicit_init: - include_bare_init: true file_length: ignore_comment_only_lines: true warning: 500 file_name: - excluded: [Group.swift, Process.swift, User.swift] + excluded: [Group.swift, InstalledApp+Spotlight.swift, Process.swift, User.swift] file_types_order: order: - main_type diff --git a/Documentation/Sample.swift b/Documentation/Sample.swift index 23e06fb6..4a58ec15 100644 --- a/Documentation/Sample.swift +++ b/Documentation/Sample.swift @@ -24,7 +24,7 @@ final class Sample { /// If the first letter of an acronym is uppercase, the entire thing should be /// uppercase. static func decode(from json: JSON) -> Self { - Self(json: json) + .init(json: json) } } diff --git a/README.md b/README.md index 96b12140..0198ecc3 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,7 @@ reattach-to-user-namespace mas install mas 2.0.0+ sources data for installed App Store apps from macOS's Spotlight Metadata Server (aka MDS). -You can check if an App Store app is properly indexed in the MDS: +You can check if an App Store app is properly indexed in Spotlight: ```console ## General format: @@ -544,7 +544,7 @@ $ mdls -rn kMDItemAppStoreAdamID /Applications/WhatsApp.app 310633997 ``` -If an app has been indexed in the MDS, the path to the app can be found: +If an app has been indexed in Spotlight, the path to the app can be found: ```shell mdfind 'kMDItemAppStoreAdamID = ' diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index f3e56fa9..529e9ecf 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -279,7 +279,7 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver { try applicationsFolderURLs.contains( where: { applicationsFolderURL in var relationship = FileManager.URLRelationship.other - try fileManager.getRelationship( + try unsafe fileManager.getRelationship( &relationship, ofDirectoryAt: applicationsFolderURL, toItemAt: appFolderURL, diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index a20990ca..7c4378a5 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -32,8 +32,8 @@ extension MAS { """ No installed apps found - If this is unexpected, any of the following command lines should fix things by reindexing apps in the\ - Spotlight MDS index (which might take some time): + If this is unexpected, any of the following command lines should fix things by reindexing apps in Spotlight\ + (which might take some time): # Individual apps (if you know exactly what apps were incorrectly omitted): mdimport /Applications/Example.app diff --git a/Sources/mas/Commands/MAS.swift b/Sources/mas/Commands/MAS.swift index c346f60f..a7a2bdf6 100644 --- a/Sources/mas/Commands/MAS.swift +++ b/Sources/mas/Commands/MAS.swift @@ -36,7 +36,7 @@ struct MAS: AsyncParsableCommand, Sendable { static let printer = Printer() static var _errorPrefix: String { // swiftlint:disable:this identifier_name - "\(format(prefix: "\(errorPrefix)", format: errorFormat, for: FileHandle.standardError)) " + "\(errorPrefix.formatted(with: errorFormat, for: FileHandle.standardError)) " } private static func main() async { // swiftlint:disable:this unused_declaration diff --git a/Sources/mas/Controllers/InstalledApp+Spotlight.swift b/Sources/mas/Controllers/InstalledApp+Spotlight.swift index 5a529aa8..f1b03caf 100644 --- a/Sources/mas/Controllers/InstalledApp+Spotlight.swift +++ b/Sources/mas/Controllers/InstalledApp+Spotlight.swift @@ -9,6 +9,32 @@ private import Atomics private import Foundation private import ObjectiveC +private extension URL { + var installedAppURLs: [URL] { + FileManager.default // swiftformat:disable indent + .enumerator(at: self, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles]) + .map { enumerator in + enumerator.compactMap { item in + guard + let url = item as? URL, + (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true, + url.lastPathComponent == "Contents" + else { + return nil as URL? + } + + enumerator.skipDescendants() + return + (try? url.appending(path: "_MASReceipt/receipt", directoryHint: .notDirectory).checkResourceIsReachable()) + == true + ? url.deletingLastPathComponent() + : nil + } + } + ?? [] + } // swiftformat:enable indent +} + var installedApps: [InstalledApp] { get async throws { try await mas.installedApps(matching: "kMDItemAppStoreAdamID LIKE '*'") @@ -53,22 +79,52 @@ func installedApps(matching metadataQuery: String) async throws -> [InstalledApp query.stop() - continuation.resume( - returning: query.results - .compactMap { result in // swiftformat:disable indent - (result as? NSMetadataItem).map { item in - InstalledApp( - adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0, - bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "", - name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "") - .removingSuffix(".app"), - path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "", - version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "", - ) + let installedApps = query.results + .compactMap { result in // swiftformat:disable indent + (result as? NSMetadataItem).map { item in + InstalledApp( + adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0, + bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "", + name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "") + .removingSuffix(".app"), + path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "", + version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "", + ) + } + } + .sorted(using: KeyPathComparator(\.name, comparator: .localizedStandard)) // swiftformat:enable indent + + if !["1", "true", "yes"].contains(ProcessInfo.processInfo.environment["MAS_NO_AUTO_INDEX"]?.lowercased()) { + let installedAppPathSet = Set(installedApps.map(\.path)) + for installedAppURL in applicationsFolderURLs.flatMap(\.installedAppURLs) + where !installedAppPathSet.contains(installedAppURL.filePath) { // swiftformat:disable:this indent + MAS.printer.warning( + "Found a likely App Store app that is not indexed in Spotlight in ", + installedAppURL.filePath, + """ + + + Indexing now, which will not complete until sometime after mas exits + + Disable auto-indexing via: export MAS_NO_AUTO_INDEX=1 + """, + separator: "", + ) + Task { + do { + _ = try await run( + "/usr/bin/mdimport", + installedAppURL.filePath, + errorMessage: "Failed to index the Spotlight data for \(installedAppURL.filePath)", + ) + } catch { + MAS.printer.error(error: error) + } } } - .sorted(using: KeyPathComparator(\.name, comparator: .localizedStandard)), // swiftformat:enable indent - ) + } + + continuation.resume(returning: installedApps) } query.start() diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 92c51282..ef93791e 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -17,7 +17,7 @@ enum MASError: Error { separator: String = ":\n", separatorAndErrorReplacement: String = "", ) -> Self { - Self.error( + .error( message, error: error.map { Self.error($0) }, separator: separator, diff --git a/Sources/mas/Network/URL.swift b/Sources/mas/Network/URL.swift index 942f3886..6cac6570 100644 --- a/Sources/mas/Network/URL.swift +++ b/Sources/mas/Network/URL.swift @@ -11,7 +11,7 @@ private import ObjectiveC extension URL { var filePath: String { - String(path(percentEncoded: false).dropLast { $0 == "/" }) + .init(path(percentEncoded: false).dropLast { $0 == "/" }) } func open(configuration: NSWorkspace.OpenConfiguration = NSWorkspace.OpenConfiguration()) async throws { diff --git a/Sources/mas/Utilities/Printer.swift b/Sources/mas/Utilities/Printer.swift index 674611d3..b673b098 100644 --- a/Sources/mas/Utilities/Printer.swift +++ b/Sources/mas/Utilities/Printer.swift @@ -124,20 +124,40 @@ struct Printer: Sendable { separator: String, terminator: String, to fileHandle: FileHandle, - ) { - let formattedPrefix = mas.format(prefix: prefix, format: format, for: fileHandle) + ) { // swiftformat:disable indent + let indent = """ + + \( + String( // swiftlint:disable:this indentation_width + repeating: " ", + count: + (prefix.range(of: "\n", options: .backwards).map { String(prefix[$0.upperBound...]) } ?? prefix).count + 1, + ) + ) + """ + + let formattedPrefix = prefix.formatted(with: format, for: fileHandle) // swiftformat:enable indent print( - items.first.map { ["\(formattedPrefix) \($0)"] + items.dropFirst().map(String.init(describing:)) } + items.first.map { item in + ["\(formattedPrefix) \(mas.indent(item, with: indent))"] + + items.dropFirst().map { mas.indent($0, with: indent) } // swiftformat:disable:this indent + } ?? [formattedPrefix], // swiftformat:disable:this indent - separator: separator, + separator: mas.indent(separator, with: indent), terminator: terminator, to: fileHandle, ) } } -func format(prefix: String, format: String, for fileHandle: FileHandle) -> String { - fileHandle.isTerminal ? "\(csi)\(format)m\(prefix)\(csi)0m" : prefix +extension String { + func formatted(with format: Self, for fileHandle: FileHandle) -> Self { + fileHandle.isTerminal ? "\(csi)\(format)m\(self)\(csi)0m" : self + } +} + +private func indent(_ item: Any, with indent: String) -> String { + .init(describing: item).replacing(unsafe nonEmptyLineStartRegex, with: indent) } let errorPrefix = "Error:" @@ -145,3 +165,5 @@ let errorFormat = "4;31" /// Terminal Control Sequence Indicator. private let csi = "\u{001B}[" + +private nonisolated(unsafe) let nonEmptyLineStartRegex = /\n(?!\n)/ diff --git a/Sources/mas/Utilities/Sudo.swift b/Sources/mas/Utilities/Sudo.swift index a9056304..a9f42764 100644 --- a/Sources/mas/Utilities/Sudo.swift +++ b/Sources/mas/Utilities/Sudo.swift @@ -17,8 +17,8 @@ func sudo(_ executableName: String, args: some Sequence) throws { try sudo([executablePath] + args) } -private func sudo(_ args: some Sequence) throws { - let cArgs = unsafe (["sudo"] + args).map { unsafe strdup($0) } // swiftformat:disable:this spaceAroundParens +private func sudo(_ args: some Sequence) throws { // swiftformat:disable:next spaceAroundParens + let cArgs = unsafe (["sudo", "MAS_NO_AUTO_INDEX=1"] + args).map { unsafe strdup($0) } defer { for unsafe cArg in unsafe cArgs { unsafe free(cArg) diff --git a/Sources/mas/Utilities/Version+SemVer.swift b/Sources/mas/Utilities/Version+SemVer.swift index 44b2cf52..89e4f146 100644 --- a/Sources/mas/Utilities/Version+SemVer.swift +++ b/Sources/mas/Utilities/Version+SemVer.swift @@ -51,15 +51,15 @@ protocol MajorMinorPatchInteger: MajorMinorPatch { extension MajorMinorPatchInteger { var major: String { - "\(majorInteger)" + .init(majorInteger) } var minor: String { - "\(minorInteger)" + .init(minorInteger) } var patch: String { - "\(patchInteger)" + .init(patchInteger) } } diff --git a/Tests/MASTests/Commands/MASTests+List.swift b/Tests/MASTests/Commands/MASTests+List.swift index 6059d3b2..17356cd7 100644 --- a/Tests/MASTests/Commands/MASTests+List.swift +++ b/Tests/MASTests/Commands/MASTests+List.swift @@ -19,17 +19,17 @@ private extension MASTests { """ Warning: No installed apps found - If this is unexpected, any of the following command lines should fix things by reindexing apps in the Spotlight\ - MDS index (which might take some time): + If this is unexpected, any of the following command lines should fix things by reindexing apps in\ + Spotlight (which might take some time): - # Individual apps (if you know exactly what apps were incorrectly omitted): - mdimport /Applications/Example.app + # Individual apps (if you know exactly what apps were incorrectly omitted): + mdimport /Applications/Example.app - # All apps ( is the volume optionally selected for large apps): - mdimport /Applications /Volumes//Applications + # All apps ( is the volume optionally selected for large apps): + mdimport /Applications /Volumes//Applications - # All file system volumes (if neither aforementioned command solved the issue): - sudo mdutil -Eai on + # All file system volumes (if neither aforementioned command solved the issue): + sudo mdutil -Eai on """, )