Skip to content
49 changes: 49 additions & 0 deletions Mist/Helpers/Codesigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
81 changes: 73 additions & 8 deletions Mist/Helpers/TaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 [
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
]
}
Expand Down
4 changes: 4 additions & 0 deletions Mist/Model/Installer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
[
Expand Down
22 changes: 1 addition & 21 deletions Mist/Views/List/InstallerExportView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
}
Expand Down
5 changes: 1 addition & 4 deletions Mist/Views/List/ListRowInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ struct ListRowInstaller: View {
}

var body: some View {
// swiftlint:disable:next closure_body_length
HStack {
ListRowDetail(
imageName: installer.imageName,
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion Mist/Views/Settings/SettingsISOsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down