使用 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