Skip to content

Commit

Permalink
Merge branch 'main' into releases/release-1.8.7
Browse files Browse the repository at this point in the history
# Conflicts:
#	.swiftlint.yml
#	script/bottle
  • Loading branch information
phatblat committed Nov 2, 2024
2 parents f9ce413 + 031e0c3 commit 750930e
Show file tree
Hide file tree
Showing 59 changed files with 548 additions and 761 deletions.
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"NeverForceUnwrap": true,
"NeverUseForceTry": true,
"NeverUseImplicitlyUnwrappedOptionals": true,
"NoAccessLevelOnExtensionDeclaration": true,
"NoAccessLevelOnExtensionDeclaration": false,
"NoAssignmentInExpressions": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
Expand Down
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.7
5.7.1
1 change: 0 additions & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

# Rule options
--commas always
--extensionacl on-declarations
--hexliteralcase lowercase
--importgrouping testable-last
--lineaftermarks false
Expand Down
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ disabled_rules:
- function_body_length
- inert_defer
- legacy_objc_type
- no_extension_access_modifier
- no_grouping_extension
- number_separator
- one_declaration_per_file
Expand Down
8 changes: 1 addition & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ CMD_NAME = mas
SHELL = /bin/sh
PREFIX ?= $(shell brew --prefix)

# trunk
# SWIFT_VERSION = swift-DEVELOPMENT-SNAPSHOT-2020-04-23-a

# Swift 5.3
# SWIFT_VERSION = swift-5.3-DEVELOPMENT-SNAPSHOT-2020-04-21-a

SWIFT_VERSION = 5.7
SWIFT_VERSION = 5.7.1

# set EXECUTABLE_DIRECTORY according to your specific environment
# run swift build and see where the output executable is created
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/PromiseKit.git",
"state" : {
"revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
"version" : "6.22.1"
"revision" : "6fcc08077124e9747f1ec7bd8bb78f5caffe5a79",
"version" : "8.1.2"
}
},
{
Expand Down
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// swift-tools-version:5.6.1
// swift-tools-version:5.7.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "mas",
platforms: [
.macOS(.v10_11)
.macOS(.v10_13)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
Expand All @@ -20,7 +20,7 @@ let package = Package(
.package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"),
.package(url: "https://github.com/Quick/Quick.git", from: "5.0.1"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"),
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "8.1.2"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
],
Expand Down
2 changes: 1 addition & 1 deletion Package/Distribution.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<options customize="never" require-scripts="false"/>
<volume-check>
<allowed-os-versions>
<os-version min="10.11"/>
<os-version min="10.13"/>
</allowed-os-versions>
</volume-check>
<choices-outline>
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ sudo port install mas
#### 🍻 Custom Homebrew tap

We provide a [custom Homebrew tap](https://github.com/mas-cli/homebrew-tap) with pre-built bottles
for all macOS versions since 10.11 (El Capitan).
for all macOS versions since 10.11 (El Capitan). The newest versions of mas, however, are only available
for macOS 10.13+ (High Sierra or newer).

To install mas from our tap:

Expand Down
61 changes: 49 additions & 12 deletions Sources/mas/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,53 @@ import CommerceKit
import PromiseKit
import StoreFoundation

/// Downloads a list of apps, one after the other, printing progress to the console.
/// Sequentially downloads apps, printing progress to the console.
///
/// Verifies that each supplied app ID is valid before attempting to download.
///
/// - Parameters:
/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
/// - searcher: The `AppStoreSearcher` used to verify app IDs.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A `Promise` that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
func downloadApps(
withAppIDs unverifiedAppIDs: [AppID],
verifiedBy searcher: AppStoreSearcher,
purchasing: Bool = false
) -> Promise<Void> {
when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
.then { results in
downloadApps(
withAppIDs:
results.compactMap { result in
switch result {
case .fulfilled(let searchResult):
return searchResult.trackId
case .rejected(let error):
printError(String(describing: error))
return nil
}
},
purchasing: purchasing
)
}
}

/// Sequentially downloads apps, printing progress to the console.
///
/// - Parameters:
/// - appIDs: The IDs of the apps to be downloaded
/// - purchase: Flag indicating whether the apps needs to be purchased.
/// Only works for free apps. Defaults to false.
/// - appIDs: The app IDs of the apps to be downloaded.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A promise that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) -> Promise<Void> {
var firstError: Error?
return
appIDs
.reduce(Guarantee.value(())) { previous, appID in
previous.then {
downloadWithRetries(appID, purchase: purchase)
downloadApp(withAppID: appID, purchasing: purchasing)
.recover { error in
if firstError == nil {
firstError = error
Expand All @@ -39,10 +71,15 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
}
}

private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> {
SSPurchase().perform(appID: appID, purchase: purchase)
private func downloadApp(
withAppID appID: AppID,
purchasing: Bool = false,
withAttemptCount attemptCount: UInt32 = 3
) -> Promise<Void> {
SSPurchase()
.perform(appID: appID, purchasing: purchasing)
.recover { error in
guard attempts > 1 else {
guard attemptCount > 1 else {
throw error
}

Expand All @@ -54,9 +91,9 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt
throw error
}

let attempts = attempts - 1
let attemptCount = attemptCount - 1
printWarning((downloadError ?? error).localizedDescription)
printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
return downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
}
}
88 changes: 15 additions & 73 deletions Sources/mas/AppStore/ISStoreAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,81 +14,23 @@ private let timeout = 30.0

extension ISStoreAccount: StoreAccount {
static var primaryAccount: Promise<ISStoreAccount> {
if #available(macOS 10.13, *) {
return race(
Promise { seal in
ISServiceProxy.genericShared().accountService
.primaryAccount { storeAccount in
seal.fulfill(storeAccount)
}
},
after(seconds: timeout)
.then {
Promise(error: MASError.notSignedIn)
race(
Promise { seal in
ISServiceProxy.genericShared().accountService
.primaryAccount { storeAccount in
seal.fulfill(storeAccount)
}
)
}

return .value(CKAccountStore.shared().primaryAccount)
},
after(seconds: timeout)
.then {
Promise(error: MASError.notSignedIn)
}
)
}

static func signIn(appleID: String, password: String, systemDialog: Bool) -> Promise<ISStoreAccount> {
// swift-format-ignore: UseEarlyExits
if #available(macOS 10.13, *) {
// Signing in is no longer possible as of High Sierra.
// https://github.com/mas-cli/mas/issues/164
return Promise(error: MASError.notSupported)
// swiftlint:disable:next superfluous_else
} else {
return
primaryAccount
.then { account -> Promise<ISStoreAccount> in
if account.isSignedIn {
return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier))
}

let password =
password.isEmpty && !systemDialog
? String(validatingUTF8: getpass("Password: ")) ?? ""
: password

guard !password.isEmpty || systemDialog else {
return Promise(error: MASError.noPasswordProvided)
}

let context = ISAuthenticationContext(accountID: 0)
context.appleIDOverride = appleID

let signInPromise =
Promise<ISStoreAccount> { seal in
let accountService = ISServiceProxy.genericShared().accountService
accountService.setStoreClient(ISStoreClient(storeClientType: 0))
accountService.signIn(with: context) { success, storeAccount, error in
if success, let storeAccount {
seal.fulfill(storeAccount)
} else {
seal.reject(MASError.signInFailed(error: error as NSError?))
}
}
}

if systemDialog {
return signInPromise
}

context.demoMode = true
context.demoAccountName = appleID
context.demoAccountPassword = password
context.demoAutologinMode = true

return race(
signInPromise,
after(seconds: timeout)
.then {
Promise(error: MASError.signInFailed(error: nil))
}
)
}
}
static func signIn(appleID _: String, password _: String, systemDialog _: Bool) -> Promise<ISStoreAccount> {
// Signing in is no longer possible as of High Sierra.
// https://github.com/mas-cli/mas/issues/164
Promise(error: MASError.notSupported)
}
}
34 changes: 30 additions & 4 deletions Sources/mas/AppStore/PurchaseDownloadObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
import CommerceKit
import StoreFoundation

private let downloadingPhase: Int64 = 0
private let installingPhase: Int64 = 1
private let downloadedPhase: Int64 = 5

@objc
class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
let purchase: SSPurchase
var completionHandler: (() -> Void)?
var errorHandler: ((MASError) -> Void)?
var priorPhaseType: Int64?

init(purchase: SSPurchase) {
self.purchase = purchase
Expand All @@ -30,6 +35,21 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
if status.isFailed || status.isCancelled {
queue.removeDownload(withItemIdentifier: download.metadata.itemIdentifier)
} else {
if priorPhaseType != status.activePhase.phaseType {
switch status.activePhase.phaseType {
case downloadedPhase:
if priorPhaseType == downloadingPhase {
clearLine()
printInfo("Downloaded \(download.progressDescription)")
}
case installingPhase:
clearLine()
printInfo("Installing \(download.progressDescription)")
default:
break
}
priorPhaseType = status.activePhase.phaseType
}
progress(status.progressState)
}
}
Expand All @@ -39,7 +59,7 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
return
}
clearLine()
printInfo("Downloading \(download.metadata.title)")
printInfo("Downloading \(download.progressDescription)")
}

func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
Expand All @@ -56,7 +76,7 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
} else if status.isCancelled {
errorHandler?(.cancelled)
} else {
printInfo("Installed \(download.metadata.title)")
printInfo("Installed \(download.progressDescription)")
completionHandler?()
}
}
Expand Down Expand Up @@ -94,6 +114,12 @@ func progress(_ state: ProgressState) {
fflush(stdout)
}

private extension SSDownload {
var progressDescription: String {
"\(metadata.title) (\(metadata.bundleVersion ?? "unknown version"))"
}
}

extension SSDownloadStatus {
var progressState: ProgressState {
ProgressState(percentComplete: percentComplete, phase: activePhase.phaseDescription)
Expand All @@ -103,9 +129,9 @@ extension SSDownloadStatus {
extension SSDownloadPhase {
var phaseDescription: String {
switch phaseType {
case 0:
case downloadingPhase:
return "Downloading"
case 1:
case installingPhase:
return "Installing"
default:
return "Waiting"
Expand Down
Loading

0 comments on commit 750930e

Please sign in to comment.