Skip to content

Commit 1ea1cbf

Browse files
authored
Merge pull request #1194 from rgoldberg/1021-installed-apps-receipts
2 parents 57ca734 + 9721e5d commit 1ea1cbf

13 files changed

Lines changed: 121 additions & 45 deletions

File tree

.swiftlint.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,11 @@ deployment_target:
4444
tvOSApplicationExtension_deployment_target: 99
4545
watchOS_deployment_target: 99
4646
watchOSApplicationExtension_deployment_target: 99
47-
explicit_init:
48-
include_bare_init: true
4947
file_length:
5048
ignore_comment_only_lines: true
5149
warning: 500
5250
file_name:
53-
excluded: [Group.swift, Process.swift, User.swift]
51+
excluded: [Group.swift, InstalledApp+Spotlight.swift, Process.swift, User.swift]
5452
file_types_order:
5553
order:
5654
- main_type

Documentation/Sample.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class Sample {
2424
/// If the first letter of an acronym is uppercase, the entire thing should be
2525
/// uppercase.
2626
static func decode(from json: JSON) -> Self {
27-
Self(json: json)
27+
.init(json: json)
2828
}
2929
}
3030

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ reattach-to-user-namespace mas install
531531
mas 2.0.0+ sources data for installed App Store apps from macOS's Spotlight
532532
Metadata Server (aka MDS).
533533

534-
You can check if an App Store app is properly indexed in the MDS:
534+
You can check if an App Store app is properly indexed in Spotlight:
535535

536536
```console
537537
## General format:
@@ -544,7 +544,7 @@ $ mdls -rn kMDItemAppStoreAdamID /Applications/WhatsApp.app
544544
310633997
545545
```
546546

547-
If an app has been indexed in the MDS, the path to the app can be found:
547+
If an app has been indexed in Spotlight, the path to the app can be found:
548548

549549
```shell
550550
mdfind 'kMDItemAppStoreAdamID = <adam-id>'

Sources/mas/AppStore/AppStoreAction+download.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver {
279279
try applicationsFolderURLs.contains(
280280
where: { applicationsFolderURL in
281281
var relationship = FileManager.URLRelationship.other
282-
try fileManager.getRelationship(
282+
try unsafe fileManager.getRelationship(
283283
&relationship,
284284
ofDirectoryAt: applicationsFolderURL,
285285
toItemAt: appFolderURL,

Sources/mas/Commands/List.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ extension MAS {
3232
"""
3333
No installed apps found
3434
35-
If this is unexpected, any of the following command lines should fix things by reindexing apps in the\
36-
Spotlight MDS index (which might take some time):
35+
If this is unexpected, any of the following command lines should fix things by reindexing apps in Spotlight\
36+
(which might take some time):
3737
3838
# Individual apps (if you know exactly what apps were incorrectly omitted):
3939
mdimport /Applications/Example.app

Sources/mas/Commands/MAS.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ struct MAS: AsyncParsableCommand, Sendable {
3636
static let printer = Printer()
3737

3838
static var _errorPrefix: String { // swiftlint:disable:this identifier_name
39-
"\(format(prefix: "\(errorPrefix)", format: errorFormat, for: FileHandle.standardError)) "
39+
"\(errorPrefix.formatted(with: errorFormat, for: FileHandle.standardError)) "
4040
}
4141

4242
private static func main() async { // swiftlint:disable:this unused_declaration

Sources/mas/Controllers/InstalledApp+Spotlight.swift

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ private import Atomics
99
private import Foundation
1010
private import ObjectiveC
1111

12+
private extension URL {
13+
var installedAppURLs: [URL] {
14+
FileManager.default // swiftformat:disable indent
15+
.enumerator(at: self, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
16+
.map { enumerator in
17+
enumerator.compactMap { item in
18+
guard
19+
let url = item as? URL,
20+
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true,
21+
url.lastPathComponent == "Contents"
22+
else {
23+
return nil as URL?
24+
}
25+
26+
enumerator.skipDescendants()
27+
return
28+
(try? url.appending(path: "_MASReceipt/receipt", directoryHint: .notDirectory).checkResourceIsReachable())
29+
== true
30+
? url.deletingLastPathComponent()
31+
: nil
32+
}
33+
}
34+
?? []
35+
} // swiftformat:enable indent
36+
}
37+
1238
var installedApps: [InstalledApp] {
1339
get async throws {
1440
try await mas.installedApps(matching: "kMDItemAppStoreAdamID LIKE '*'")
@@ -53,22 +79,52 @@ func installedApps(matching metadataQuery: String) async throws -> [InstalledApp
5379

5480
query.stop()
5581

56-
continuation.resume(
57-
returning: query.results
58-
.compactMap { result in // swiftformat:disable indent
59-
(result as? NSMetadataItem).map { item in
60-
InstalledApp(
61-
adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0,
62-
bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "",
63-
name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "")
64-
.removingSuffix(".app"),
65-
path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "",
66-
version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "",
67-
)
82+
let installedApps = query.results
83+
.compactMap { result in // swiftformat:disable indent
84+
(result as? NSMetadataItem).map { item in
85+
InstalledApp(
86+
adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0,
87+
bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "",
88+
name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "")
89+
.removingSuffix(".app"),
90+
path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "",
91+
version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "",
92+
)
93+
}
94+
}
95+
.sorted(using: KeyPathComparator(\.name, comparator: .localizedStandard)) // swiftformat:enable indent
96+
97+
if !["1", "true", "yes"].contains(ProcessInfo.processInfo.environment["MAS_NO_AUTO_INDEX"]?.lowercased()) {
98+
let installedAppPathSet = Set(installedApps.map(\.path))
99+
for installedAppURL in applicationsFolderURLs.flatMap(\.installedAppURLs)
100+
where !installedAppPathSet.contains(installedAppURL.filePath) { // swiftformat:disable:this indent
101+
MAS.printer.warning(
102+
"Found a likely App Store app that is not indexed in Spotlight in ",
103+
installedAppURL.filePath,
104+
"""
105+
106+
107+
Indexing now, which will not complete until sometime after mas exits
108+
109+
Disable auto-indexing via: export MAS_NO_AUTO_INDEX=1
110+
""",
111+
separator: "",
112+
)
113+
Task {
114+
do {
115+
_ = try await run(
116+
"/usr/bin/mdimport",
117+
installedAppURL.filePath,
118+
errorMessage: "Failed to index the Spotlight data for \(installedAppURL.filePath)",
119+
)
120+
} catch {
121+
MAS.printer.error(error: error)
122+
}
68123
}
69124
}
70-
.sorted(using: KeyPathComparator(\.name, comparator: .localizedStandard)), // swiftformat:enable indent
71-
)
125+
}
126+
127+
continuation.resume(returning: installedApps)
72128
}
73129

74130
query.start()

Sources/mas/Errors/MASError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enum MASError: Error {
1717
separator: String = ":\n",
1818
separatorAndErrorReplacement: String = "",
1919
) -> Self {
20-
Self.error(
20+
.error(
2121
message,
2222
error: error.map { Self.error($0) },
2323
separator: separator,

Sources/mas/Network/URL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ private import ObjectiveC
1111

1212
extension URL {
1313
var filePath: String {
14-
String(path(percentEncoded: false).dropLast { $0 == "/" })
14+
.init(path(percentEncoded: false).dropLast { $0 == "/" })
1515
}
1616

1717
func open(configuration: NSWorkspace.OpenConfiguration = NSWorkspace.OpenConfiguration()) async throws {

Sources/mas/Utilities/Printer.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,46 @@ struct Printer: Sendable {
124124
separator: String,
125125
terminator: String,
126126
to fileHandle: FileHandle,
127-
) {
128-
let formattedPrefix = mas.format(prefix: prefix, format: format, for: fileHandle)
127+
) { // swiftformat:disable indent
128+
let indent = """
129+
130+
\(
131+
String( // swiftlint:disable:this indentation_width
132+
repeating: " ",
133+
count:
134+
(prefix.range(of: "\n", options: .backwards).map { String(prefix[$0.upperBound...]) } ?? prefix).count + 1,
135+
)
136+
)
137+
"""
138+
139+
let formattedPrefix = prefix.formatted(with: format, for: fileHandle) // swiftformat:enable indent
129140
print(
130-
items.first.map { ["\(formattedPrefix) \($0)"] + items.dropFirst().map(String.init(describing:)) }
141+
items.first.map { item in
142+
["\(formattedPrefix) \(mas.indent(item, with: indent))"]
143+
+ items.dropFirst().map { mas.indent($0, with: indent) } // swiftformat:disable:this indent
144+
}
131145
?? [formattedPrefix], // swiftformat:disable:this indent
132-
separator: separator,
146+
separator: mas.indent(separator, with: indent),
133147
terminator: terminator,
134148
to: fileHandle,
135149
)
136150
}
137151
}
138152

139-
func format(prefix: String, format: String, for fileHandle: FileHandle) -> String {
140-
fileHandle.isTerminal ? "\(csi)\(format)m\(prefix)\(csi)0m" : prefix
153+
extension String {
154+
func formatted(with format: Self, for fileHandle: FileHandle) -> Self {
155+
fileHandle.isTerminal ? "\(csi)\(format)m\(self)\(csi)0m" : self
156+
}
157+
}
158+
159+
private func indent(_ item: Any, with indent: String) -> String {
160+
.init(describing: item).replacing(unsafe nonEmptyLineStartRegex, with: indent)
141161
}
142162

143163
let errorPrefix = "Error:"
144164
let errorFormat = "4;31"
145165

146166
/// Terminal Control Sequence Indicator.
147167
private let csi = "\u{001B}["
168+
169+
private nonisolated(unsafe) let nonEmptyLineStartRegex = /\n(?!\n)/

0 commit comments

Comments
 (0)