From 2212ff5ba62827602fe84e96fd67538f3e95bd00 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:36:56 -0500 Subject: [PATCH 1/5] Cleanup Swift: Insert missing `unsafe`. Use `.init` instead of explicit type inits. Infer `.error` type instead of using explicit type. Replace unnecessary string interpolation. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 2 -- Documentation/Sample.swift | 2 +- Sources/mas/AppStore/AppStoreAction+download.swift | 2 +- Sources/mas/Commands/MAS.swift | 2 +- Sources/mas/Errors/MASError.swift | 2 +- Sources/mas/Network/URL.swift | 2 +- Sources/mas/Utilities/Version+SemVer.swift | 6 +++--- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index e1a24a5d6..931703420 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -44,8 +44,6 @@ 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 diff --git a/Documentation/Sample.swift b/Documentation/Sample.swift index 23e06fb6a..4a58ec15f 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/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index f3e56fa96..529e9ecf2 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/MAS.swift b/Sources/mas/Commands/MAS.swift index c346f60fb..906c89d20 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)) " + "\(format(prefix: errorPrefix, format: errorFormat, for: FileHandle.standardError)) " } private static func main() async { // swiftlint:disable:this unused_declaration diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 92c512823..ef93791e4 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 942f38869..6cac6570d 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/Version+SemVer.swift b/Sources/mas/Utilities/Version+SemVer.swift index 44b2cf528..89e4f1469 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) } } From 44489bddad9323aae7baa47aa96492ad1bc4c716 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:52:08 -0500 Subject: [PATCH 2/5] Simplify references to Spotlight/MDS. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- README.md | 4 ++-- Sources/mas/Commands/List.swift | 4 ++-- Tests/MASTests/Commands/MASTests+List.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96b12140a..0198ecc33 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/Commands/List.swift b/Sources/mas/Commands/List.swift index a20990caf..7c4378a56 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/Tests/MASTests/Commands/MASTests+List.swift b/Tests/MASTests/Commands/MASTests+List.swift index 6059d3b29..9bc225974 100644 --- a/Tests/MASTests/Commands/MASTests+List.swift +++ b/Tests/MASTests/Commands/MASTests+List.swift @@ -19,8 +19,8 @@ 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 From 635cc955630e76313fda383501b4f8b547a4325d Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:56:08 -0500 Subject: [PATCH 3/5] Refactor `format(prefix:format:for:)` as `String.formatted(with:for:)`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/MAS.swift | 2 +- Sources/mas/Utilities/Printer.swift | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/mas/Commands/MAS.swift b/Sources/mas/Commands/MAS.swift index 906c89d20..a7a2bdf65 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/Utilities/Printer.swift b/Sources/mas/Utilities/Printer.swift index 674611d31..81591b597 100644 --- a/Sources/mas/Utilities/Printer.swift +++ b/Sources/mas/Utilities/Printer.swift @@ -125,7 +125,7 @@ struct Printer: Sendable { terminator: String, to fileHandle: FileHandle, ) { - let formattedPrefix = mas.format(prefix: prefix, format: format, for: fileHandle) + let formattedPrefix = prefix.formatted(with: format, for: fileHandle) print( items.first.map { ["\(formattedPrefix) \($0)"] + items.dropFirst().map(String.init(describing:)) } ?? [formattedPrefix], // swiftformat:disable:this indent @@ -136,8 +136,10 @@ struct Printer: Sendable { } } -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 + } } let errorPrefix = "Error:" From 38f1bc96c37632c21e6e46ce51e50dcb911fbf98 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:58:24 -0500 Subject: [PATCH 4/5] Indent subsequent lines for `Printer` output that has a prefix. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Utilities/Printer.swift | 28 ++++++++++++++++++--- Tests/MASTests/Commands/MASTests+List.swift | 16 ++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Sources/mas/Utilities/Printer.swift b/Sources/mas/Utilities/Printer.swift index 81591b597..b673b0980 100644 --- a/Sources/mas/Utilities/Printer.swift +++ b/Sources/mas/Utilities/Printer.swift @@ -124,12 +124,26 @@ struct Printer: Sendable { separator: String, terminator: String, to fileHandle: FileHandle, - ) { - let formattedPrefix = prefix.formatted(with: 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, ) @@ -142,8 +156,14 @@ extension String { } } +private func indent(_ item: Any, with indent: String) -> String { + .init(describing: item).replacing(unsafe nonEmptyLineStartRegex, with: indent) +} + let errorPrefix = "Error:" let errorFormat = "4;31" /// Terminal Control Sequence Indicator. private let csi = "\u{001B}[" + +private nonisolated(unsafe) let nonEmptyLineStartRegex = /\n(?!\n)/ diff --git a/Tests/MASTests/Commands/MASTests+List.swift b/Tests/MASTests/Commands/MASTests+List.swift index 9bc225974..17356cd72 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 Spotlight\ - (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 """, ) From 9721e5d96b5e31dec9ab449e44fb88cfb269badc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:01:52 -0500 Subject: [PATCH 5/5] Index in Spotlight folders that should contain App Store apps but that aren't indexed as such in Spotlight. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 2 +- .../Controllers/InstalledApp+Spotlight.swift | 84 +++++++++++++++---- Sources/mas/Utilities/Sudo.swift | 4 +- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 931703420..5319f5b9c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -48,7 +48,7 @@ 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/Sources/mas/Controllers/InstalledApp+Spotlight.swift b/Sources/mas/Controllers/InstalledApp+Spotlight.swift index 5a529aa84..f1b03cafa 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/Utilities/Sudo.swift b/Sources/mas/Utilities/Sudo.swift index a9056304c..a9f427640 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)