import AppKit struct InstallError: LocalizedError { let errorDescription: String? } do { guard CommandLine.arguments.count == 3, !CommandLine.arguments[2].isEmpty else { throw InstallError(errorDescription: "Usage: swift Scripts/install.swift ") } let manager = FileManager.default let identifier = "dev.conjure.launcher" let source = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true) .standardizedFileURL.resolvingSymlinksInPath() let directory = URL(fileURLWithPath: (CommandLine.arguments[2] as NSString).expandingTildeInPath, isDirectory: true) .standardizedFileURL.resolvingSymlinksInPath() let destination = directory.appendingPathComponent("Conjure.app", isDirectory: true) guard source.path != destination.path else { throw InstallError(errorDescription: "The build and installation paths must be different.") } guard Bundle(url: source)?.bundleIdentifier == identifier else { throw InstallError(errorDescription: "The source is not a Conjure application bundle.") } func validateDestination() throws -> Bool { let exists = manager.fileExists(atPath: destination.path) if exists { guard try destination.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink != true, Bundle(url: destination)?.bundleIdentifier == identifier else { throw InstallError(errorDescription: "Refusing to replace an unrelated app or symbolic link at \(destination.path).") } } let running = NSRunningApplication.runningApplications(withBundleIdentifier: identifier) .contains { $0.bundleURL?.resolvingSymlinksInPath().path == destination.path } guard !running else { throw InstallError(errorDescription: "Quit the installed Conjure at \(destination.path) before reinstalling.") } return exists } _ = try validateDestination() try manager.createDirectory(at: directory, withIntermediateDirectories: true) let staging = directory.appendingPathComponent(".Conjure-install-\(UUID().uuidString).app", isDirectory: true) defer { try? manager.removeItem(at: staging) } try manager.copyItem(at: source, to: staging) let verification = Process() verification.executableURL = URL(fileURLWithPath: "/usr/bin/codesign") verification.arguments = ["--verify", "--strict", staging.path] try verification.run() verification.waitUntilExit() guard verification.terminationStatus == 0 else { throw InstallError(errorDescription: "Bundle signature verification failed; the installed app was not changed.") } if try validateDestination() { _ = try manager.replaceItemAt(destination, withItemAt: staging, options: .usingNewMetadataOnly) } else { try manager.moveItem(at: staging, to: destination) } print("Installed Conjure at \(destination.path).") } catch { FileHandle.standardError.write(Data("Installation failed: \(error.localizedDescription)\n".utf8)) exit(1) }