diff --git a/Mist/Helpers/Codesigner.swift b/Mist/Helpers/Codesigner.swift index 9b4252b..1f9ea3f 100644 --- a/Mist/Helpers/Codesigner.swift +++ b/Mist/Helpers/Codesigner.swift @@ -24,4 +24,53 @@ enum Codesigner { throw MistError.invalidTerminationStatus(status: response.terminationStatus, output: response.standardOutput, error: response.standardError) } } + + /// Sign the provided URL with an ad-hoc code signature. + /// + /// - Parameters: + /// - url: The URL of the file or directory to sign with an ad-hoc code signature. + /// + /// - Throws: A `MistError` if the provided URL is invalid or a command failed to execute. + static func adHocCodesign(_ url: URL) throws { + guard + let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator( + at: url, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) else { + throw MistError.invalidURL(url.path) + } + + for case let url as URL in enumerator { + let fileAttributes: URLResourceValues = try url.resourceValues(forKeys: [.isRegularFileKey]) + + guard + let isRegularFile: Bool = fileAttributes.isRegularFile, + isRegularFile else { + continue + } + + do { + let arguments: [String] = ["codesign", "--remove-signature", "--force", url.path] + let response: HelperToolCommandResponse = try ShellExecutor.shared.execute(arguments) + + guard response.terminationStatus == 0 else { + throw MistError.invalidTerminationStatus(status: response.terminationStatus, output: response.standardOutput, error: response.standardError) + } + } catch { + // do nothing + } + + do { + let arguments: [String] = ["codesign", "--sign", "-", "--force", url.path] + let response: HelperToolCommandResponse = try ShellExecutor.shared.execute(arguments) + + guard response.terminationStatus == 0 else { + throw MistError.invalidTerminationStatus(status: response.terminationStatus, output: response.standardOutput, error: response.standardError) + } + } catch { + // do nothing + } + } + } } diff --git a/Mist/Helpers/TaskManager.swift b/Mist/Helpers/TaskManager.swift index b53dd86..bb60fb8 100644 --- a/Mist/Helpers/TaskManager.swift +++ b/Mist/Helpers/TaskManager.swift @@ -250,7 +250,7 @@ class TaskManager: ObservableObject { ), ( section: .bootableInstaller, - tasks: bootableInstallerTasks(for: installer, volume: volume) + tasks: bootableInstallerTasks(for: installer, temporaryDirectory: temporaryDirectoryURL, volume: volume) ), ( section: .cleanup, @@ -446,9 +446,11 @@ class TaskManager: ObservableObject { // swiftlint:disable:next function_body_length private static func isoTasks(for installer: Installer, filename: String, destination destinationURL: URL, temporaryDirectory temporaryDirectoryURL: URL) -> [MistTask] { let temporaryImageURL: URL = temporaryDirectoryURL.appendingPathComponent("\(installer.id).dmg") - let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/Resources/createinstallmedia") + let createInstallMediaAppendingPathComponent: String = "Contents/Resources/createinstallmedia" + let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent(createInstallMediaAppendingPathComponent) let temporaryCDRURL: URL = temporaryDirectoryURL.appendingPathComponent("\(installer.id).cdr") let isoURL: URL = destinationURL.appendingPathComponent(filename.stringWithSubstitutions(name: installer.name, version: installer.version, build: installer.build)) + let temporaryInstallerWithAdHocCodeSignaturesURL: URL = temporaryDirectoryURL.appendingPathComponent("Install \(installer.name).app") // Same name for ad-hoc signed app allows ISO to boot without modifying plists if installer.mavericksOrNewer { return [ @@ -467,10 +469,39 @@ class TaskManager: ObservableObject { LogManager.shared.log(.info, message: "Updating Property List '\(infoPlistURL.path)'...") try PropertyListUpdater.update(infoPlistURL, key: "CFBundleShortVersionString", value: "12.6.03") } + + var createInstallMediaURLToUse: URL = createInstallMediaURL + // Workaround to make OS X Mavericks 10.9 to macOS Catalina 10.15 createinstallmedia work on Apple Silicon + if + let architecture: Architecture = Hardware.architecture, + architecture == .appleSilicon, !installer.bigSurOrNewer { + LogManager.shared.log(.info, message: "Copying '\(installer.temporaryInstallerURL.path)' to '\(temporaryInstallerWithAdHocCodeSignaturesURL.path)'...") + try FileManager.default.copyItem(at: installer.temporaryInstallerURL, to: temporaryInstallerWithAdHocCodeSignaturesURL) + + LogManager.shared.log(.info, message: "Ad-hoc code signing '\(temporaryInstallerWithAdHocCodeSignaturesURL.path)'...") + try Codesigner.adHocCodesign(temporaryInstallerWithAdHocCodeSignaturesURL) + + createInstallMediaURLToUse = temporaryInstallerWithAdHocCodeSignaturesURL.appendingPathComponent(createInstallMediaAppendingPathComponent) + } // swiftlint:disable:next line_length - LogManager.shared.log(.info, message: "Creating macOS Installer in temporary Disk Image at mount point '\(installer.temporaryISOMountPointURL.path)' using createinstallmedia '\(createInstallMediaURL.path)'...") - try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: installer.temporaryISOMountPointURL, sierraOrOlder: installer.sierraOrOlder) + LogManager.shared.log(.info, message: "Creating macOS Installer in temporary Disk Image at mount point '\(installer.temporaryISOMountPointURL.path)' using createinstallmedia '\(createInstallMediaURLToUse.path)'...") + try await InstallMediaCreator.create(createInstallMediaURLToUse, mountPoint: installer.temporaryISOMountPointURL, sierraOrOlder: installer.sierraOrOlder) + + if + let architecture: Architecture = Hardware.architecture, + architecture == .appleSilicon, !installer.bigSurOrNewer { + for url in [ + temporaryInstallerWithAdHocCodeSignaturesURL, + installer.temporaryISOInstallerURL + ] where FileManager.default.fileExists(atPath: url.path) { + LogManager.shared.log(.info, message: "Deleting '\(url.path)'...") + try FileManager.default.removeItem(at: url) + } + + LogManager.shared.log(.info, message: "Copying '\(installer.temporaryInstallerURL.path)' to '\(installer.temporaryISOInstallerURL.path)'...") + try FileManager.default.copyItem(at: installer.temporaryInstallerURL, to: installer.temporaryISOInstallerURL) + } }, MistTask(type: .unmount, description: "temporary Disk Image") { if FileManager.default.fileExists(atPath: installer.temporaryISOMountPointURL.path) { @@ -556,9 +587,13 @@ class TaskManager: ObservableObject { return tasks } - private static func bootableInstallerTasks(for installer: Installer, volume: InstallerVolume) -> [MistTask] { - let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/Resources/createinstallmedia") + private static func bootableInstallerTasks(for installer: Installer, temporaryDirectory temporaryDirectoryURL: URL, volume: InstallerVolume) -> [MistTask] { + let createInstallMediaAppendingPathComponent: String = "Contents/Resources/createinstallmedia" + let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent(createInstallMediaAppendingPathComponent) let mountPointURL: URL = .init(fileURLWithPath: volume.path) + let installerNameAppendingPathComponent: String = "Install \(installer.name).app" // Same name for ad-hoc signed app allows installer to boot without modifying plists + let temporaryInstallerWithAdHocCodeSignaturesURL: URL = temporaryDirectoryURL.appendingPathComponent(installerNameAppendingPathComponent) + let bootableInstallerURL: URL = mountPointURL.deletingLastPathComponent().appendingPathComponent("Install \(installer.name)").appendingPathComponent(installerNameAppendingPathComponent) return [ MistTask(type: .create, description: "Bootable Installer") { // Workaround to make macOS Sierra 10.12 createinstallmedia work @@ -567,9 +602,39 @@ class TaskManager: ObservableObject { LogManager.shared.log(.info, message: "Updating Property List '\(infoPlistURL.path)'...") try PropertyListUpdater.update(infoPlistURL, key: "CFBundleShortVersionString", value: "12.6.03") } + + var createInstallMediaURLToUse: URL = createInstallMediaURL + // Workaround to make OS X Mavericks 10.9 to macOS Catalina 10.15 createinstallmedia work on Apple Silicon + if + let architecture: Architecture = Hardware.architecture, + architecture == .appleSilicon, !installer.bigSurOrNewer { + LogManager.shared.log(.info, message: "Copying '\(installer.temporaryInstallerURL.path)' to '\(temporaryInstallerWithAdHocCodeSignaturesURL.path)'...") + try FileManager.default.copyItem(at: installer.temporaryInstallerURL, to: temporaryInstallerWithAdHocCodeSignaturesURL) + + LogManager.shared.log(.info, message: "Ad-hoc code signing '\(temporaryInstallerWithAdHocCodeSignaturesURL.path)'...") + try Codesigner.adHocCodesign(temporaryInstallerWithAdHocCodeSignaturesURL) + + createInstallMediaURLToUse = temporaryInstallerWithAdHocCodeSignaturesURL.appendingPathComponent(createInstallMediaAppendingPathComponent) + } - LogManager.shared.log(.info, message: "Creating Bootable Installer at mount point '\(mountPointURL.path)' using createinstallmedia '\(createInstallMediaURL.path)'...") - try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: mountPointURL, sierraOrOlder: installer.sierraOrOlder) + LogManager.shared.log(.info, message: "Creating Bootable Installer at mount point '\(mountPointURL.path)' using createinstallmedia '\(createInstallMediaURLToUse.path)'...") + try await InstallMediaCreator.create(createInstallMediaURLToUse, mountPoint: mountPointURL, sierraOrOlder: installer.sierraOrOlder) + + if + let architecture: Architecture = Hardware.architecture, + architecture == .appleSilicon, !installer.bigSurOrNewer { + + for url in [ + temporaryInstallerWithAdHocCodeSignaturesURL, + bootableInstallerURL + ] { + LogManager.shared.log(.info, message: "Deleting '\(url.path)'...") + try FileManager.default.removeItem(at: url) + } + + LogManager.shared.log(.info, message: "Copying '\(installer.temporaryInstallerURL.path)' to '\(bootableInstallerURL.path)'...") + try FileManager.default.copyItem(at: installer.temporaryInstallerURL, to: bootableInstallerURL) + } } ] } diff --git a/Mist/Model/Installer.swift b/Mist/Model/Installer.swift index e37cc85..f59b7ae 100644 --- a/Mist/Model/Installer.swift +++ b/Mist/Model/Installer.swift @@ -752,6 +752,10 @@ struct Installer: Decodable, Hashable, Identifiable { var temporaryISOMountPointURL: URL { URL(fileURLWithPath: "/Volumes/Install \(name)") } + + var temporaryISOInstallerURL: URL { + temporaryISOMountPointURL.appendingPathComponent("Install \(name).app") + } var dictionary: [String: Any] { [ diff --git a/Mist/Views/List/InstallerExportView.swift b/Mist/Views/List/InstallerExportView.swift index c4eeeed..0ddcdfd 100644 --- a/Mist/Views/List/InstallerExportView.swift +++ b/Mist/Views/List/InstallerExportView.swift @@ -18,17 +18,6 @@ struct InstallerExportView: View { private var exportPackage: Bool = false var installer: Installer @Binding var exports: [InstallerExportType] - private var isoCompatible: Bool { - guard let architecture: Architecture = Hardware.architecture else { - return false - } - - return architecture == .intel || (architecture == .appleSilicon && installer.bigSurOrNewer) - } - - private var compatibilityMessage: String { - "**Note:** ISOs are unavailable for building **macOS Catalina 10.15 and older** on [Apple Silicon Macs](https://support.apple.com/en-us/HT211814)." - } var body: some View { VStack { @@ -41,16 +30,11 @@ struct InstallerExportView: View { InstallerExportViewItem(exportType: .diskImage, selected: $exportDiskImage) .disabled(exports.count == 1 && exportDiskImage) InstallerExportViewItem(exportType: .iso, selected: $exportISO) - .disabled(isoCompatible ? exports.count == 1 && exportISO : true) - .opacity(isoCompatible ? 1 : 0.5) + .disabled(exports.count == 1 && exportISO) InstallerExportViewItem(exportType: .package, selected: $exportPackage) .disabled(exports.count == 1 && exportPackage) Spacer() } - if !isoCompatible { - Text(.init(compatibilityMessage)) - .padding(.top) - } } .padding() .onChange(of: exportApplication) { _ in @@ -73,10 +57,6 @@ struct InstallerExportView: View { private func updateExports() { var exports: [InstallerExportType] = [] - if !isoCompatible, exportISO { - exportISO = false - } - if !exportApplication, !exportDiskImage, !exportISO, !exportPackage { exportApplication = true } diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index 88a89d8..cee4c4f 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -73,7 +73,6 @@ struct ListRowInstaller: View { } var body: some View { - // swiftlint:disable:next closure_body_length HStack { ListRowDetail( imageName: installer.imageName, @@ -92,9 +91,7 @@ struct ListRowInstaller: View { } .help("Download and export macOS Installer") .buttonStyle(.mistAction) - if - let architecture: Architecture = Hardware.architecture, - (architecture == .appleSilicon && installer.bigSurOrNewer) || (architecture == .intel && installer.mavericksOrNewer) { + if installer.mavericksOrNewer { Button { pressButton(.volumeSelection) } label: { diff --git a/Mist/Views/Settings/SettingsISOsView.swift b/Mist/Views/Settings/SettingsISOsView.swift index b05209a..023a0cb 100644 --- a/Mist/Views/Settings/SettingsISOsView.swift +++ b/Mist/Views/Settings/SettingsISOsView.swift @@ -13,7 +13,7 @@ struct SettingsISOsView: View { private let imageName: String = "ISO" private let title: String = "ISOs" // swiftlint:disable:next line_length - private let description: String = "ISOs are Bootable macOS Installer Disk Images that can be restored on external USB drives, or used with virtualization software (ie. [Parallels Desktop](https://www.parallels.com/au/products/desktop/), [UTM](https://mac.getutm.app), [VMware Fusion](https://www.vmware.com/au/products/fusion.html), [VirtualBox](https://www.virtualbox.org)).\n\n**Note:** ISOs are unavailable for building **macOS Catalina 10.15 and older** on [Apple Silicon Macs](https://support.apple.com/en-us/HT211814)." + private let description: String = "ISOs are Bootable macOS Installer Disk Images that can be restored on external USB drives, or used with virtualization software (ie. [Parallels Desktop](https://www.parallels.com/au/products/desktop/), [UTM](https://mac.getutm.app), [VMware Fusion](https://www.vmware.com/au/products/fusion.html), [VirtualBox](https://www.virtualbox.org)).\n\n**Note:** ISOs are unavailable for building **OS X Mountain Lion 10.8 and older** on [Apple Silicon Macs](https://support.apple.com/en-us/HT211814)." var body: some View { VStack(alignment: .leading) {