使用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:
(Xcode produces a non-thinned universal app), (Xcode produces a universal app and all available thinned variants), or a model identifier for a specific device (e.g. "iPhone7,1"). Defaults to
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