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: 1 addition & 3 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Documentation/Sample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 = <adam-id>'
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/AppStore/AppStoreAction+download.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Sources/mas/Commands/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/MAS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 70 additions & 14 deletions Sources/mas/Controllers/InstalledApp+Spotlight.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 '*'")
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Errors/MASError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Network/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 28 additions & 6 deletions Sources/mas/Utilities/Printer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,46 @@ 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:"
let errorFormat = "4;31"

/// Terminal Control Sequence Indicator.
private let csi = "\u{001B}["

private nonisolated(unsafe) let nonEmptyLineStartRegex = /\n(?!\n)/
4 changes: 2 additions & 2 deletions Sources/mas/Utilities/Sudo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func sudo(_ executableName: String, args: some Sequence<String>) throws {
try sudo([executablePath] + args)
}

private func sudo(_ args: some Sequence<String>) throws {
let cArgs = unsafe (["sudo"] + args).map { unsafe strdup($0) } // swiftformat:disable:this spaceAroundParens
private func sudo(_ args: some Sequence<String>) 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)
Expand Down
6 changes: 3 additions & 3 deletions Sources/mas/Utilities/Version+SemVer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
16 changes: 8 additions & 8 deletions Tests/MASTests/Commands/MASTests+List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<LargeAppVolume> is the volume optionally selected for large apps):
mdimport /Applications /Volumes/<LargeAppVolume>/Applications
# All apps (<LargeAppVolume> is the volume optionally selected for large apps):
mdimport /Applications /Volumes/<LargeAppVolume>/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

""",
)
Expand Down
Loading