使用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 常用文件内容格式
compileBitcode
For non-App Store exports, should Xcode re-compile the app from bitcode? Defaults to YES
embedOnDemandResourcesAssetPacksInBundle
For 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 specified
method
Describes 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 development
teamID
The Developer Portal team to use for this export. Defaults to the team used to build the archive
thinning
For 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>
uploadBitcode
For App Store exports, should the package include bitcode? Defaults to YES
uploadSymbols
For App Store exports, should the package include symbols? Defaults to YES
发表回复