def checkVersion_(self, mountPoint): LogDebug(u"checkVersion:%@", mountPoint) # We're now examining InstallESD.dmg for 10.7/10.8, BaseSystem.dmg for # 10.9, or a system image. name, version, build = IEDUtil.readSystemVersion_(mountPoint) if self.baseSystemMountedFromPath: self.dmgHelper.detach_selector_(self.baseSystemMountedFromPath, self.handleDetachResult_) installerVersion = tuple(int(x) for x in version.split(u".")) runningVersion = tuple( int(x) for x in platform.mac_ver()[0].split(u".")) LogNotice(u"Found source: %@ %@ %@", name, version, build) if installerVersion[:2] != runningVersion[:2]: self.delegate.ejectingSource() self.dmgHelper.detachAll_(self.rejectSource_) return LogNotice(u"Accepted source %@: %@ %@ %@", self.newSourcePath, name, version, build) self._source = self.newSourcePath self.installerName = name self.installerVersion = version self.installerBuild = build info = { u"name": name, u"version": version, u"build": build, u"template": self.loadImageTemplate_(mountPoint), u"sourceType": self.sourceType, } self.delegate.sourceSucceeded_(info) # There's no reason to keep the dmg mounted if it's not an installer. if self.sourceType == IEDWorkflow.SYSTEM_IMAGE: self.dmgHelper.detachAll_(self.ejectSystemImage_)
def nextTask(self): LogDebug("nextTask, currentTask == %@", self.currentTask) if self.currentTask: if self.currentTask["phases"]: for phase in self.currentTask["phases"]: if not phase.get("optional", False): details = NSString.stringWithFormat_( "Phases remaining: %@", self.currentTask["phases"]) self.fail_details_("Task finished prematurely", details) return if self.tasks: self.currentTask = self.tasks.pop(0) LogNotice("Starting task %@ with %d phases", self.currentTask["title"], len(self.currentTask["phases"])) self.nextPhase() LogDebug("Calling %@()", self.currentTask["title"]) self.currentTask["method"]() LogDebug("Returned from %@()", self.currentTask["title"]) else: LogNotice("Build finished successfully, image saved to %@", self.outputPath()) self.delegate.buildSucceeded() self.stop()
def launchScript_(self, args): LogDebug("launchScript:") if (self.authPassword() is None) and (os.getuid() != 0): # Use GUI dialog to elevate privileges. task = STPrivilegedTask.alloc().init() task.setLaunchPath_(args[0]) task.setArguments_(args[1:]) status = task.launch() LogNotice("Install task launched with return code: %d", status) if status: self.performSelectorOnMainThread_withObject_waitUntilDone_( self.handleLaunchScriptError_, status, False) else: task = NSTask.alloc().init() if os.getuid() == 0: # No privilege elevation necessary. task.setLaunchPath_(args[0]) task.setArguments_(args[1:]) else: # Use sudo to elevate privileges. task.setLaunchPath_("/usr/bin/sudo") task.setArguments_(["-kSE"] + args) # Send password to sudo on stdin. passwordpipe = NSPipe.alloc().init() task.setStandardInput_(passwordpipe.fileHandleForReading()) task.setStandardOutput_( NSFileHandle.fileHandleWithNullDevice()) task.setStandardError_(NSFileHandle.fileHandleWithNullDevice()) writer = passwordpipe.fileHandleForWriting() pwd = NSString.stringWithString_(self.authPassword() + "\n") writer.writeData_(pwd.dataUsingEncoding_(NSUTF8StringEncoding)) writer.closeFile() try: task.launch() LogNotice("Install task launched%@", " with sudo" if os.getuid() != 0 else "") except BaseException as e: LogWarning("Install task launch failed with exception") self.performSelectorOnMainThread_withObject_waitUntilDone_( self.handleLaunchScriptError_, str(e), False) return task.waitUntilExit() LogNotice("Install task finished with exit status %d", task.terminationStatus()) if task.terminationStatus() != 0: self.performSelectorOnMainThread_withObject_waitUntilDone_( self.handleLaunchScriptError_, "Install task failed with status %d" % task.terminationStatus(), False)
def sourceSucceeded_(self, info): self.installerName = info[u"name"] self.installerVersion = info[u"version"] self.installerBuild = info[u"build"] LogNotice(u"Found installer: %s %s %s" % (info[u"name"], info[u"version"], info[u"build"])) self.busy = False
def taskFinalize(self): LogNotice(u"Finalize task running") self.delegate.buildSetProgressMessage_( u"Scanning disk image for restore") # The script is wrapped with progresswatcher.py which parses script # output and sends it back as notifications to IEDSocketListener. args = [ NSBundle.mainBundle().pathForResource_ofType_( u"progresswatcher", u"py"), u"--socket", self.listenerPath, u"imagescan", self.outputPath(), ] LogInfo(u"Launching finalize with arguments:") for arg in args: LogInfo(u" '%@'", arg) try: p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out = p.communicate()[0].decode(u"utf-8") LogDebug(u"Finalize exited with status %d and output '%@'", p.returncode, out) if p.returncode != 0: errMsg = u"Finalize task failed with status %d" % p.returncode LogError(u"%@: %@", errMsg, out) self.fail_details_(errMsg, out) except BaseException as e: LogError(u"Failed to launch finalize task: %@", unicode(e)) self.fail_details_(u"Failed to launch finalize task", unicode(e))
def taskInstall(self): LogNotice("Install task running") # The script is wrapped with progresswatcher.py which parses script # output and sends it back as notifications to IEDSocketListener. args = [ NSBundle.mainBundle().pathForResource_ofType_( "progresswatcher", "py"), "--cd", NSBundle.mainBundle().resourcePath(), "--socket", self.listenerPath, "installesdtodmg", "--user", str(os.getuid()), "--group", str(os.getgid()), "--fstype", self.filesystem(), "--output", self.outputPath(), "--volume-name", self.volumeName(), "--size", str(self.volumeSize()), "--template", self.templatePath, ] if self.sourceType == IEDWorkflow.SYSTEM_IMAGE: args.extend(["--baseimage", self.source()]) args.extend(self.packagesToInstall) LogInfo("Launching install with arguments:") for arg in args: LogInfo(" '%@'", arg) self.performSelectorInBackground_withObject_(self.launchScript_, args)
def connectionDidFinishLoading_(self, connection): LogInfo(u"%@ finished downloading to %@", self.package.name(), self.cacheTmpPath_(self.package.sha1())) self.fileHandle.closeFile() self.delegate.downloadStopped_(self.package) if self.checksum.hexdigest() == self.package.sha1(): try: os.rename(self.cacheTmpPath_(self.package.sha1()), self.cachePath_(self.package.sha1())) except OSError as e: error = u"Failed when moving download to %s: %s" % ( self.cachePath_(self.package.sha1()), unicode(e)) LogError(error) self.delegate.downloadFailed_withError_(self.package, error) return linkPath = self.updatePath_(self.package.sha1()) try: os.symlink(self.package.sha1(), linkPath) except OSError as e: error = u"Failed when creating link from %s to %s: %s" % ( self.package.sha1(), linkPath, unicode(e)) LogError(error) self.delegate.downloadFailed_withError_(self.package, error) return LogNotice(u"%@ added to cache with sha1 %@", self.package.name(), self.package.sha1()) self.delegate.downloadSucceeded_(self.package) self.downloadNextUpdate() else: error = u"Expected sha1 checksum %s but got %s" % ( sha1.lower(), m.hexdigest().lower()) LogError(error) self.delegate.downloadFailed_withError_(self.package, error)
def cmdDownload_(self, args): """Download updates""" profile = self.profileController.profileForVersion_Build_( args.version, args.build) if profile is None: self.failWithMessage_( self.profileController.whyNoProfileForVersion_build_( args.version, args.build)) return os.EX_DATAERR updates = list() for update in profile: if not self.cache.isCached_(update["sha1"]): package = IEDPackage.alloc().init() package.setName_(update["name"]) package.setPath_(self.cache.updatePath_(update["sha1"])) package.setSize_(update["size"]) package.setUrl_(update["url"]) package.setSha1_(update["sha1"]) updates.append(package) if updates: self.cache.downloadUpdates_(updates) self.busy = True self.waitBusy() if self.hasFailed: return 1 # EXIT_FAILURE LogNotice("All updates for %@ %@ downloaded", args.version, args.build) return os.EX_OK
def sourceSucceeded_(self, info): self.installerName = info["name"] self.installerVersion = info["version"] self.installerBuild = info["build"] LogNotice("Found installer: %@ %@ %@", info["name"], info["version"], info["build"]) self.busy = False
def cmdList_(self, args): """List updates""" profile = self.profileController.profileForVersion_Build_(args.version, args.build) if profile is None: self.failWithMessage_(self.profileController.whyNoProfileForVersion_build_(args.version, args.build)) return os.EX_DATAERR LogNotice(u"%d update%@ for %@ %@:", len(profile), u"" if len(profile) == 1 else u"s", args.version, args.build) for update in profile: LogNotice(u" %@%@ (%@)", u"[cached] " if self.cache.isCached_(update[u"sha1"]) else u"", update[u"name"], IEDUtil.formatBytes_(update[u"size"])) return os.EX_OK
def socketReceivedMessage_(self, msg): # The message is a dictionary with "action" as the only required key. action = msg["action"] if action == "update_progress": percent = msg["percent"] currentProgress = self.progress + self.currentPhase[ "weight"] * percent / 100.0 self.delegate.buildSetProgress_(currentProgress) elif action == "update_message": if self.lastUpdateMessage != msg["message"]: # Only log update messages when they change. LogInfo("%@", msg["message"]) self.lastUpdateMessage = msg["message"] self.delegate.buildSetProgressMessage_(msg["message"]) elif action == "select_phase": LogNotice("Script phase: %@", msg["phase"]) self.nextPhase() elif action == "log_message": LogMessage(msg["log_level"], msg["message"]) elif action == "notify_failure": self.fail_details_("Build failed", msg["message"]) elif action == "notify_success": LogNotice("Build success: %@", msg["message"]) elif action == "task_done": status = msg["termination_status"] if status == 0: self.nextTask() else: details = NSString.stringWithFormat_( "Task exited with status %@", msg["termination_status"]) LogError("%@", details) # Status codes 100-199 are from installesdtodmg.sh, and have # been preceeded by a "notify_failure" message. if (status < 100) or (status > 199): self.fail_details_("Build failed", details) else: self.fail_details_("Unknown progress notification", "Message: %@", msg)
def profileForVersion_Build_(self, version, build): """Return the update profile for a certain OS X version and build.""" try: profile = self.profiles["%s-%s" % (version, build)] LogInfo("Update profile for %@ %@: %@", version, build, ", ".join(u["name"] for u in profile)) except KeyError: profile = None LogNotice("No update profile for %@ %@", version, build) return profile
def checkVersion_(self, mountPoint): LogDebug("checkVersion:%@", mountPoint) # We're now examining InstallESD.dmg for 10.7/10.8, BaseSystem.dmg for # 10.9, or a system image. name, version, build = IEDUtil.readSystemVersion_(mountPoint) if self.baseSystemMountedFromPath: self.dmgHelper.detach_selector_(self.baseSystemMountedFromPath, self.handleDetachResult_) installerVersion = IEDUtil.splitVersion(version) runningVersion = IEDUtil.hostVersionTuple() LogNotice("Found source: %@ %@ %@", name, version, build) if installerVersion[:2] != runningVersion[:2]: self.delegate.ejectingSource() majorVersion = ".".join(str(x) for x in installerVersion[:2]) self.delegate.sourceFailed_text_("OS version mismatch", "The major version of the installer and the current OS must match.\n\n" + \ "%s %s %s installer requires running %s.x to build an image." % ( name, version, build, majorVersion)) self.dmgHelper.detachAll_(self.alertFailedUnmounts_) return LogNotice("Accepted source %@: %@ %@ %@", self.newSourcePath, name, version, build) self._source = self.newSourcePath self.installerName = name self.installerVersion = version self.installerBuild = build info = { "name": name, "version": version, "build": build, "template": self.loadImageTemplate_(mountPoint), "sourceType": self.sourceType, } self.delegate.sourceSucceeded_(info) # There's no reason to keep the dmg mounted if it's not an InstallESD. if self.sourceType != IEDWorkflow.INSTALL_ESD: self.dmgHelper.detachAll_(self.ejectSystemImage_)
def taskFinalize(self): LogNotice("Finalize task running") self.delegate.buildSetProgressMessage_("Scanning disk image for restore") # The script is wrapped with progresswatcher.py which parses script # output and sends it back as notifications to IEDSocketListener. args = [ NSBundle.mainBundle().pathForResource_ofType_("progresswatcher", "py"), "--socket", self.listenerPath, "imagescan", self.outputPath(), ] LogInfo("Launching finalize with arguments:") for arg in args: LogInfo(" '%@'", arg) self.performSelectorInBackground_withObject_(self.launchFinalize_, args)
def taskFinalize(self): LogNotice(u"Finalize task running") self.delegate.buildSetProgressMessage_(u"Scanning disk image for restore") # The script is wrapped with progresswatcher.py which parses script # output and sends it back as notifications to IEDSocketListener. args = [ NSBundle.mainBundle().pathForResource_ofType_(u"progresswatcher", u"py"), u"--socket", self.listenerPath, u"imagescan", self.outputPath(), ] LogInfo(u"Launching finalize with arguments:") for arg in args: LogInfo(u" '%@'", arg) subprocess.Popen(args)
def nextPhase(self): LogDebug("nextPhase, currentPhase == %@", self.currentPhase) if self.currentPhase: self.progress += self.currentPhase["weight"] LogInfo("Phase %@ with weight %ld finished after %.3f seconds", self.currentPhase["title"], self.currentPhase["weight"], time.time() - self.phaseStartTime) self.phaseStartTime = time.time() try: self.currentPhase = self.currentTask["phases"].pop(0) except IndexError: self.fail_details_("No phase left in task", traceback.format_stack()) return LogNotice("Starting phase: %@", self.currentPhase["title"]) self.delegate.buildSetPhase_(self.currentPhase["title"]) self.delegate.buildSetProgress_(self.progress)
def connectionDidFinishLoading_(self, connection): LogDebug("Downloaded profile with %d bytes", self.profileUpdateData.length()) if self.profileUpdateWindow: # Hide the progress window. self.profileUpdateWindow.orderOut_(self) # Decode the plist. plist, format, error = NSPropertyListSerialization.propertyListWithData_options_format_error_( self.profileUpdateData, NSPropertyListImmutable, None, None) if not plist: self.delegate.profileUpdateFailed_(error) return LogNotice("Downloaded update profiles with PublicationDate %@", plist["PublicationDate"]) # Update the user's profiles if it's newer. latestProfiles = self.updateUsersProfilesIfNewer_(plist) # Load the latest profiles. self.loadProfilesFromPlist_(latestProfiles) # Notify delegate. self.delegate.profileUpdateSucceeded_( latestProfiles["PublicationDate"]) self.delegate.profileUpdateAllDone()
def cmdBuild_(self, args): """Build image""" # Parse arguments. sourcePath = IEDUtil.installESDPath_(args.source) or \ IEDUtil.systemImagePath_(args.source) if sourcePath: templatePath = None else: templatePath = self.checkTemplate_(args.source) if not sourcePath and not templatePath: self.failWithMessage_( "'%s' is not a valid OS X installer, OS X system image or AutoDMG template" % args.source) return os.EX_DATAERR if templatePath: template = IEDTemplate.alloc().init() error = template.loadTemplateAndReturnError_(templatePath) if error: self.failWithMessage_("Couldn't load template from '%s': %s" % (templatePath, error)) return os.EX_DATAERR else: template = IEDTemplate.alloc().initWithSourcePath_(sourcePath) if args.installer: template.setSourcePath_(args.installer) if args.output: template.setOutputPath_(args.output) if args.name: template.setVolumeName_(args.name) if args.size: template.setVolumeSize_(args.size) if args.skip_asr_imagescan: template.setFinalizeAsrImagescan_(False) if args.filesystem: template.setFilesystem_(args.filesystem) if args.updates is not None: template.setApplyUpdates_(True) if args.packages: if not template.setAdditionalPackages_(args.packages): self.failWithMessage_( "Additional packages failed verification: %s" % template.additionalPackageError) return os.EX_DATAERR if not template.sourcePath: self.failWithMessage_("No source path") return os.EX_USAGE if not template.outputPath: self.failWithMessage_("No output path") return os.EX_USAGE LogNotice("Installer: %@", template.sourcePath) # Set the source. self.busy = True self.workflow.setSource_(template.sourcePath) self.waitBusy() if self.hasFailed: return os.EX_DATAERR template.resolveVariables_({ "OSNAME": self.installerName, "OSVERSION": self.installerVersion, "OSBUILD": self.installerBuild, }) LogNotice("Output Path: %@", template.outputPath) LogNotice("Volume Name: %@", template.volumeName) # Generate the list of updates to install. updates = list() if template.applyUpdates: profile = self.profileController.profileForVersion_Build_( self.installerVersion, self.installerBuild) if profile is None: self.failWithMessage_( self.profileController.whyNoProfileForVersion_build_( self.installerVersion, self.installerBuild)) return os.EX_DATAERR missingUpdates = list() for update in profile: LogNotice("Update: %@ (%@)", update["name"], IEDUtil.formatByteSize_(update["size"])) package = IEDPackage.alloc().init() package.setName_(update["name"]) package.setPath_(self.cache.updatePath_(update["sha1"])) package.setSize_(update["size"]) package.setUrl_(update["url"]) package.setSha1_(update["sha1"]) if not self.cache.isCached_(update["sha1"]): if args.download_updates: missingUpdates.append(package) else: self.failWithMessage_( "Can't apply updates, %s is missing from cache" % update["name"]) return os.EX_DATAERR updates.append(package) if missingUpdates: self.cache.downloadUpdates_(missingUpdates) self.busy = True self.waitBusy() if self.hasFailed: self.failWithMessage_( "Can't build due to updates missing from cache") return 1 # EXIT_FAILURE updates.extend(missingUpdates) LogNotice("All updates for %@ %@ downloaded", self.installerVersion, self.installerBuild) # Generate the list of additional packages to install. template.resolvePackages() for package in template.packagesToInstall: LogNotice("Package: %@ (%@)", package.name(), IEDUtil.formatByteSize_(package.size())) # Check the output path. if os.path.exists(template.outputPath): if args.force: try: os.unlink(template.outputPath) except OSError as e: self.failWithMessage_("Couldn't remove %s: %s" % (template.outputPath, str(e))) return os.EX_CANTCREAT else: self.failWithMessage_("%s already exists" % template.outputPath) return os.EX_CANTCREAT else: outputDir = os.path.dirname(template.outputPath) if outputDir and not os.path.exists(outputDir): try: os.makedirs(outputDir) except OSError as e: self.failWithMessage_( "%s does not exist and can't be created: %s" % (outputDir, str(e))) return os.EX_CANTCREAT # If we're not running as root get the password for authentication. if os.getuid() != 0: username = NSUserName() currentUser = CBIdentity.identityWithName_authority_( username, CBIdentityAuthority.defaultIdentityAuthority()) passwordOK = False while not passwordOK: password = getpass.getpass("Password for %s: " % username).decode("utf-8") if currentUser.authenticateWithPassword_(password): passwordOK = True self.workflow.setAuthUsername_(username) self.workflow.setAuthPassword_(password) # Start the workflow. self.busy = True self.workflow.setPackagesToInstall_(updates + template.packagesToInstall) self.workflow.setOutputPath_(template.outputPath) self.workflow.setVolumeName_(template.volumeName) self.workflow.setVolumeSize_(template.volumeSize) self.workflow.setFinalizeAsrImagescan_(template.finalizeAsrImagescan) self.workflow.setFilesystem_(template.filesystem) self.workflow.setTemplate_(template) self.workflow.start() self.waitBusy() if self.hasFailed: return 1 # EXIT_FAILURE return os.EX_OK
def downloadStarting_(self, package): LogNotice("Downloading %@ (%@)", package.name(), IEDUtil.formatByteSize_(package.size())) self.lastProgressPercent = -100.0 self.lastProgressTimestamp = NSDate.alloc().init()
def buildSucceeded(self): LogNotice("Build successful")
def buildSetPhase_(self, phase): LogNotice("phase: %@", phase)
def start(self): LogNotice("Starting build") LogNotice("Using installer: %@ %@ %@", self.installerName, self.installerVersion, self.installerBuild) LogNotice("Using output path: %@", self.outputPath()) LogNotice("TMPDIR is set to: %@", os.getenv("TMPDIR")) self.delegate.buildStartingWithOutput_(self.outputPath()) self.createTempDir() LogDebug("Created temporary directory at %@", self.tempDir) if not self.template(): self.fail_details_( "Template missing", "A template for inclusion in the image is required.") return datestamp = datetime.datetime.today().strftime("%Y%m%d") self.templatePath = os.path.join(self.tempDir, "AutoDMG-%s.adtmpl" % datestamp) LogDebug("Saving template to %@", self.templatePath) error = self.template().saveTemplateAndReturnError_(self.templatePath) if error: self.fail_details_("Couldn't save template to tempdir", error) return # The workflow is split into tasks, and each task has one or more # phases. Each phase of the installation is given a weight for the # progress bar, calculated from the size of the installer package. # Phases that don't install packages get an estimated weight. self.tasks = list() # Prepare for install. self.tasks.append({ "title": "Prepare", "method": self.taskPrepare, "phases": [ { "title": "Preparing", "weight": 34 * 1024 * 1024 }, ], }) # Perform installation. installerPhases = [ { "title": "Starting install", "weight": 21 * 1024 * 1024 }, { "title": "Creating disk image", "weight": 21 * 1024 * 1024 }, ] if self.sourceType != IEDWorkflow.SYSTEM_IMAGE: installerPhases.append({ "title": "Installing OS", "weight": 4 * 1024 * 1024 * 1024, }) for package in self.additionalPackages: installerPhases.append({ "title": "Installing %s" % package.name(), # Add 100 MB to the weight to account for overhead. "weight": package.size() + 100 * 1024 * 1024, }) installerPhases.extend([ # hdiutil convert. { "title": "Converting disk image", "weight": 313 * 1024 * 1024 }, ]) self.tasks.append({ "title": "Install", "method": self.taskInstall, "phases": installerPhases, }) # Finalize image. (Skip adding this task if Finalize: Scan for restore is unchecked.) if self._finalizeAsrImagescan: self.tasks.append({ "title": "Finalize", "method": self.taskFinalize, "phases": [ { "title": "Scanning disk image", "weight": 2 * 1024 * 1024 }, { "title": "Scanning disk image", "weight": 1 * 1024 * 1024 }, { "title": "Scanning disk image", "weight": 150 * 1024 * 1024 }, { "title": "Scanning disk image", "weight": 17 * 1024 * 1024, "optional": True }, ], }) # Finish build. self.tasks.append({ "title": "Finish", "method": self.taskFinish, "phases": [ { "title": "Finishing", "weight": 1 * 1024 * 1024 }, ], }) # Calculate total weight of all phases. self.totalWeight = 0 for task in self.tasks: LogInfo("Task %@ with %d phases:", task["method"].__name__, len(task["phases"])) for phase in task["phases"]: LogInfo(" Phase '%@' with weight %.1f", phase["title"], phase["weight"] / 1048576.0) self.totalWeight += phase["weight"] self.delegate.buildSetTotalWeight_(self.totalWeight) # Start the first task. self.progress = 0 self.currentTask = None self.currentPhase = None self.nextTask()
def connectionDidFinishLoading_(self, connection): LogDebug("Downloaded version check data with %d bytes", self.plistData.length()) # Decode the plist. plist, format, error = NSPropertyListSerialization.propertyListWithData_options_format_error_( self.plistData, NSPropertyListImmutable, None, None) if not plist: self.logFailure_(error.localizedDescription()) return # Save the time stamp. self.defaults.setObject_forKey_(NSDate.date(), "LastAppVersionCheck") # Get latest version and build. latestDisplayVersion = plist["Version"] if latestDisplayVersion.count(".") == 1: latestPaddedVersion = latestDisplayVersion + ".0" else: latestPaddedVersion = latestDisplayVersion latestBuild = plist["Build"] latestVersionBuild = "%s.%s" % (latestPaddedVersion, latestBuild) LogNotice("Latest published version is AutoDMG v%@ build %@", latestDisplayVersion, latestBuild) if self.checkSilently: # Check if we've already notified the user about this version. if latestVersionBuild == self.defaults.stringForKey_( "NotifiedAppVersion"): LogDebug("User has already been notified of this version.") return # Convert latest version into a tuple with (major, minor, rev, build). latestTuple = IEDUtil.splitVersion(latestVersionBuild, strip="ab") # Get the current version and convert it to a tuple. displayVersion, build = IEDUtil.getAppVersion() if displayVersion.count(".") == 1: paddedVersion = displayVersion + ".0" else: paddedVersion = displayVersion versionBuild = "%s.%s" % (paddedVersion, build) currentTuple = IEDUtil.splitVersion(versionBuild, strip="ab") # Compare and notify if latestTuple > currentTuple: alert = NSAlert.alloc().init() alert.setMessageText_("A new version of AutoDMG is available") alert.setInformativeText_( "AutoDMG v%s build %s is available for download." % (latestDisplayVersion, latestBuild)) alert.addButtonWithTitle_("Download") alert.addButtonWithTitle_("Skip") alert.addButtonWithTitle_("Later") button = alert.runModal() if button == NSAlertFirstButtonReturn: url = NSURL.URLWithString_(plist["URL"]) NSWorkspace.sharedWorkspace().openURL_(url) elif button == NSAlertSecondButtonReturn: self.defaults.setObject_forKey_(latestVersionBuild, "NotifiedAppVersion") elif not self.checkSilently: alert = NSAlert.alloc().init() alert.setMessageText_("AutoDMG is up to date") if currentTuple > latestTuple: verString = "bleeding edge" else: verString = "current" alert.setInformativeText_( "AutoDMG v%s build %s appears to be %s." % (displayVersion, build, verString)) alert.runModal()
def taskFinish(self): LogNotice("Finish") self.delegate.buildSetProgress_(self.totalWeight) self.nextTask()