使用dmg安装macos app打包出的app运行如下图,使用磁盘压缩成dmg,直接打开package.dmg即可配置完毕后点击start运行打包脚本,生成ipa到指定目录该项目用swift开发,项目和dmg保存在https://github.com/gwh111/tes…流程解析概述整个流程就是,通过recoverAndSet()函数恢复之前保存数据,start()检查路径后会替换内部package.sh的动态路径,然后起一个线程创建Process(),通过Pipe()监控脚本执行输出,捕获异常1.recoverAndSet()通过UserDefaults简单地记住上次打包的路径,下次写了新代码后即可点击start立即打包恢复时把值传给控件func recoverAndSet() { let objs:[Any]=[projectPath,projectName,exportOptionsPath,ipaPath] let names:[NSString]=[“projectPath”,“projectName”,“exportOptionsPath”,“ipaPath”] for i in 0…3{ print(i) let key=names[i] let obj=objs[i] as! NSTextField let v=UserDefaults.standard.value(forKey: key as String) if (v == nil){ continue } obj.stringValue=(v as? String)! } let ps=UserDefaults.standard.value(forKey: “projectName” as String) if (ps==nil){ }else{ projectName.stringValue=(ps as? String)!; } let dr=UserDefaults.standard.value(forKey: “debugRelease”) if (dr==nil){ }else{ debugRelease.selectedSegment=dr as! Int; } debugRelease.action = #selector(segmentControlChanged(segmentControl:)) }2.selectPath()通过NSOpenPanel()创建打开文档面板对象,选择文件目录,而不是手动输入通常项目路径名和项目名称是一致的,这里使用了path.components(separatedBy:"/")将路径分割自动取工程名@IBAction func selectPath(_ sender: NSButton) { let tag=sender.tag print(tag) // 1. 创建打开文档面板对象 let openPanel = NSOpenPanel() // 2. 设置确认按钮文字 openPanel.prompt = “Select” // 3. 设置禁止选择文件 openPanel.canChooseFiles = true if tag==0||tag==2 { openPanel.canChooseFiles = false } // 4. 设置可以选择目录 openPanel.canChooseDirectories = true if tag==1 { openPanel.canChooseDirectories = false openPanel.allowedFileTypes=[“plist”] } // 5. 弹出面板框 openPanel.beginSheetModal(for: self.view.window!) { (result) in // 6. 选择确认按钮 if result == NSApplication.ModalResponse.OK { // 7. 获取选择的路径 let path=openPanel.urls[0].absoluteString.removingPercentEncoding! if tag==0 { self.projectPath.stringValue=path let array=path.components(separatedBy:"/") if array.count>1{ let name=array[array.count-2] print(array) print(name as Any) self.projectName.stringValue=name } }else if tag==1 { self.exportOptionsPath.stringValue=path }else{ self.ipaPath.stringValue=path } let names:[NSString]=[“projectPath”,“exportOptionsPath”,“ipaPath”] UserDefaults.standard.setValue(openPanel.url?.path, forKey: names[tag] as String) UserDefaults.standard.setValue(self.projectName.stringValue, forKey: “projectName”) UserDefaults.standard.synchronize() // self.savePath.stringValue = (openPanel.directoryURL?.path)!// // 8. 保存用户选择路径(为了可以在其他地方有权限访问这个路径,需要对用户选择的路径进行保存)// UserDefaults.standard.setValue(openPanel.url?.path, forKey: kSelectedFilePath)// UserDefaults.standard.synchronize() } // 9. 恢复按钮状态// sender.state = NSOffState } }3.start()通过str.replacingOccurrences(of: “file://”, with: “")将路径和sh里的路径替换通过DispatchQueue.global(qos: .default).async获取Concurrent Dispatch Queue并开启Process()在处理完的terminationHandler里回到主线程更新UI@IBAction func start(_ sender: Any) { guard projectPath.stringValue != "” else { self.logTextField.stringValue=“工程目录不能为空”; return } guard projectName.stringValue != "" else { self.logTextField.stringValue=“工程名不能为空”; return } guard exportOptionsPath.stringValue != "" else { self.logTextField.stringValue=“exportOptions不能为空 xcode生成ipa文件夹中包含”; return } guard ipaPath.stringValue != "" else { self.logTextField.stringValue=“输出ipa目录不能为空”; return } var str1=“abc” let str2=“abc” if str1==str2{ print(“same”) } //save let objs:[Any]=[projectPath,exportOptionsPath,ipaPath] let names:[NSString]=[“projectPath”,“exportOptionsPath”,“ipaPath”] for i in 0…2{ let obj=objs[i] as! NSTextField UserDefaults.standard.setValue(obj.stringValue, forKey: names[i] as String) } UserDefaults.standard.setValue(self.projectName.stringValue, forKey: “projectName”) UserDefaults.standard.setValue(self.debugRelease.selectedSegment, forKey: “debugRelease”) UserDefaults.standard.synchronize() // self.showInfoTextView.string=“abc”; if isLoadingRepo { self.logTextField.stringValue=“正在执行上一个任务”; return }// 如果正在执行,则返回 isLoadingRepo = true // 设置正在执行标记 let projectStr=self.projectPath.stringValue let nameStr=self.projectName.stringValue let plistStr=self.exportOptionsPath.stringValue let ipaStr=self.ipaPath.stringValue let returnData = Bundle.main.path(forResource: “package”, ofType: “sh”) let data = NSData.init(contentsOfFile: returnData!) var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String if debugRelease.selectedSegment==0 { str = str.replacingOccurrences(of: “DEBUG_RELEASE”, with: “debug”) }else{ str = str.replacingOccurrences(of: “DEBUG_RELEASE”, with: “release”) } str = str.replacingOccurrences(of: “NAME_PROJECT”, with: nameStr) str = str.replacingOccurrences(of: “PATH_PROJECT”, with: projectStr) str = str.replacingOccurrences(of: “PATH_PLIST”, with: plistStr) str = str.replacingOccurrences(of: “PATH_IPA”, with: ipaStr) str = str.replacingOccurrences(of: “file://”, with: “”) print(“返回的数据:(str)”); self.logTextField.stringValue=“执行中。。。”; DispatchQueue.global(qos: .default).async { // str=“aaaabc”// str = str.replacingOccurrences(of: “ab”, with: “dd”) // print(self.projectPath.stringValue)// print(self.exportOptionsPath.stringValue)// print(self.ipaPath.stringValue) let task = Process() // 创建NSTask对象 // 设置task task.launchPath = “/bin/bash” // 执行路径(这里是需要执行命令的绝对路径) // 设置执行的具体命令 task.arguments = ["-c",str] task.terminationHandler = { proce in // 执行结束的闭包(回调) self.isLoadingRepo = false // 恢复执行标记 //5. 在主线程处理UI DispatchQueue.main.async(execute: { self.logTextField.stringValue=“执行完毕”; }) } self.captureStandardOutputAndRouteToTextView(task) task.launch() // 开启执行 task.waitUntilExit() // 阻塞直到执行完毕 } }4.captureStandardOutputAndRouteToTextView()对执行脚本的日志监控为了看到脚本报错或执行成功提示,使用Pipe()监控 NSPipe一般是两个线程之间进行通信使用的在osx 系统中 ,沙盒有个规则:在App运行期间通过NSOpenPanel用户手动打开的任意位置的文件,把这个这个路径保存下来,后面都是可以直接用这个路径继续访问文件,但当App退出后再次运行,这个路径默认是不可以访问的fileprivate func captureStandardOutputAndRouteToTextView(_ task:Process) { //1. 设置标准输出管道 outputPipe = Pipe() task.standardOutput = outputPipe //2. 在后台线程等待数据和通知 outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() //3. 接受到通知消息 observe=NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in //4. 获取管道数据 转为字符串 let output = self.outputPipe.fileHandleForReading.availableData let outputString = String(data: output, encoding: String.Encoding.utf8) ?? "" if outputString != “”{ //5. 在主线程处理UI DispatchQueue.main.async { if self.isLoadingRepo == false { let previousOutput = self.showInfoTextView.string let nextOutput = previousOutput + “\n” + outputString self.showInfoTextView.string = nextOutput // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) if self.observe==nil { return } NotificationCenter.default.removeObserver(self.observe!) return }else{ let previousOutput = self.showInfoTextView.string var nextOutput = previousOutput + “\n” + outputString as String if nextOutput.count>5000 { nextOutput=String(nextOutput.suffix(1000)); } // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) self.showInfoTextView.string = nextOutput } } } if self.isLoadingRepo == false { return } //6. 继续等待新数据和通知 self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() } }-exportOptions.Plist 常用文件内容格式compileBitcodeFor non-App Store exports, should Xcode re-compile the app from bitcode? Defaults to YESembedOnDemandResourcesAssetPacksInBundleFor non-App Store exports, if the app uses On Demand Resources and this is YES, asset packs are embedded in the app bundle so that the app can be tested without a server to host asset packs. Defaults to YES unless onDemandResourcesAssetPacksBaseURL is specifiedmethodDescribes how Xcode should export the archive. Available options: app-store, ad-hoc, package, enterprise, development, and developer-id. The list of options varies based on the type of archive. Defaults to developmentteamIDThe Developer Portal team to use for this export. Defaults to the team used to build the archivethinningFor non-App Store exports, should Xcode thin the package for one or more device variants? Available options: <none> (Xcode produces a non-thinned universal app), <thin-for-all-variants> (Xcode produces a universal app and all available thinned variants), or a model identifier for a specific device (e.g. “iPhone7,1”). Defaults to <none>uploadBitcodeFor App Store exports, should the package include bitcode? Defaults to YESuploadSymbolsFor App Store exports, should the package include symbols? Defaults to YES
...